myPrayerJournal v2 #27
| @ -4,6 +4,130 @@ open FSharp.Control.Tasks.V2.ContextInsensitive | |||||||
| open Microsoft.EntityFrameworkCore | open Microsoft.EntityFrameworkCore | ||||||
| open Microsoft.FSharpLu | open Microsoft.FSharpLu | ||||||
| 
 | 
 | ||||||
|  | open Newtonsoft.Json | ||||||
|  | open Raven.Client.Documents.Indexes | ||||||
|  | open System | ||||||
|  | open System.Collections.Generic | ||||||
|  | 
 | ||||||
|  | /// JSON converter for request IDs | ||||||
|  | type RequestIdJsonConverter() = | ||||||
|  |   inherit JsonConverter<RequestId>() | ||||||
|  |   override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) = | ||||||
|  |     (string >> writer.WriteValue) value | ||||||
|  |   override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) = | ||||||
|  |     (string >> RequestId.fromIdString) reader.Value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// JSON converter for user IDs | ||||||
|  | type UserIdJsonConverter() = | ||||||
|  |   inherit JsonConverter<UserId>() | ||||||
|  |   override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) = | ||||||
|  |     (string >> writer.WriteValue) value | ||||||
|  |   override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) = | ||||||
|  |     (string >> UserId) reader.Value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// JSON converter for Ticks | ||||||
|  | type TicksJsonConverter() = | ||||||
|  |   inherit JsonConverter<Ticks>() | ||||||
|  |   override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) = | ||||||
|  |     writer.WriteValue (value.toLong ()) | ||||||
|  |   override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) = | ||||||
|  |     (string >> int64 >> Ticks) reader.Value | ||||||
|  | 
 | ||||||
|  | /// Index episodes by their series Id | ||||||
|  | type Requests_ByUserId () as this = | ||||||
|  |   inherit AbstractJavaScriptIndexCreationTask () | ||||||
|  |   do | ||||||
|  |     this.Maps <- HashSet<string> [ "map('Requests', function (req) { return { userId : req.userId } })" ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Extensions on the IAsyncDocumentSession interface to support our data manipulation needs | ||||||
|  | [<AutoOpen>] | ||||||
|  | module Extensions = | ||||||
|  |    | ||||||
|  |   open Raven.Client.Documents.Commands.Batches | ||||||
|  |   open Raven.Client.Documents.Operations | ||||||
|  |   open Raven.Client.Documents.Session | ||||||
|  |   open System | ||||||
|  | 
 | ||||||
|  |   /// Format an RQL query by a strongly-typed index | ||||||
|  |   let fromIndex (typ : Type) = | ||||||
|  |     typ.Name.Replace ("_", "/") |> sprintf "from index '%s'" | ||||||
|  | 
 | ||||||
|  |   /// Utility method to create patch requests | ||||||
|  |   let createPatch<'T> collName itemId (item : 'T) = | ||||||
|  |     let r = PatchRequest() | ||||||
|  |     r.Script          <- sprintf "this.%s.push(args.Item)" collName | ||||||
|  |     r.Values.["Item"] <- item | ||||||
|  |     PatchCommandData (itemId, null, r, null) | ||||||
|  | 
 | ||||||
|  |   // Extensions for the RavenDB session type | ||||||
|  |   type IAsyncDocumentSession with | ||||||
|  |      | ||||||
|  |     /// Add a history entry | ||||||
|  |     member this.AddHistory (reqId : RequestId) (hist : History) = | ||||||
|  |       createPatch "history" (string reqId) hist | ||||||
|  |       |> this.Advanced.Defer | ||||||
|  | 
 | ||||||
|  |     /// Add a request | ||||||
|  |     member this.AddRequest req = | ||||||
|  |       this.StoreAsync (req, req.Id) | ||||||
|  | 
 | ||||||
|  |     /// Retrieve all answered requests for the given user | ||||||
|  |     // TODO: not right | ||||||
|  |     member this.AnsweredRequests (userId : UserId) = | ||||||
|  |       sprintf "%s where userId = '%s' and lastStatus = 'Answered' order by asOf as long desc" | ||||||
|  |         (fromIndex typeof<Requests_ByUserId>) (string userId) | ||||||
|  |       |> this.Advanced.AsyncRawQuery<JournalRequest> | ||||||
|  |      | ||||||
|  |     /// Retrieve the user's current journal | ||||||
|  |     // TODO: probably not right either | ||||||
|  |     member this.JournalByUserId (userId : UserId) = | ||||||
|  |       sprintf "%s where userId = '%s' and lastStatus <> 'Answered' order by showAfter as long" | ||||||
|  |         (fromIndex typeof<Requests_ByUserId>) (string userId) | ||||||
|  |       |> this.Advanced.AsyncRawQuery<JournalRequest> | ||||||
|  |      | ||||||
|  |     /// Retrieve a request by its ID and user ID | ||||||
|  |     member this.TryRequestById (reqId : RequestId) userId = | ||||||
|  |       task { | ||||||
|  |         let! req = this.LoadAsync (string reqId) | ||||||
|  |         match Option.fromObject req with | ||||||
|  |         | Some r when r.userId = userId -> return Some r | ||||||
|  |         | _ -> return None | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /// Retrieve notes for a request by its ID and user ID | ||||||
|  |     member this.NotesById reqId userId = | ||||||
|  |       task { | ||||||
|  |         match! this.TryRequestById reqId userId with | ||||||
|  |         | Some _ -> return this.Notes.AsNoTracking().Where(fun n -> n.requestId = reqId) |> List.ofSeq | ||||||
|  |         | None -> return [] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /// Retrieve a journal request by its ID and user ID | ||||||
|  |     member this.TryJournalById reqId userId = | ||||||
|  |       task { | ||||||
|  |         let! req = this.Journal.FirstOrDefaultAsync(fun r -> r.requestId = reqId && r.userId = userId) | ||||||
|  |         return Option.fromObject req | ||||||
|  |         } | ||||||
|  |      | ||||||
|  |     /// Retrieve a request, including its history and notes, by its ID and user ID | ||||||
|  |     member this.TryFullRequestById requestId userId = | ||||||
|  |       task { | ||||||
|  |         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 Option.fromObject fullReq with | ||||||
|  |             | Some _ -> return Some { req with history = List.ofSeq fullReq.history; notes = List.ofSeq fullReq.notes } | ||||||
|  |             | None -> return None | ||||||
|  |         | None -> return None | ||||||
|  |         } | ||||||
|  | 
 | ||||||
| /// Entity Framework configuration for myPrayerJournal | /// Entity Framework configuration for myPrayerJournal | ||||||
| module internal EFConfig = | module internal EFConfig = | ||||||
|    |    | ||||||
|  | |||||||
							
								
								
									
										158
									
								
								src/MyPrayerJournal.Api/Domain.fs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/MyPrayerJournal.Api/Domain.fs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,158 @@ | |||||||
|  | [<AutoOpen>] | ||||||
|  | /// The data model for myPrayerJournal | ||||||
|  | module MyPrayerJournal.Domain | ||||||
|  | 
 | ||||||
|  | /// A Collision-resistant Unique IDentifier | ||||||
|  | type Cuid = | ||||||
|  |   | Cuid of string | ||||||
|  | with | ||||||
|  |   /// The string value of the CUID | ||||||
|  |   override x.ToString () = match x with Cuid y -> y | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Request ID is a CUID | ||||||
|  | type RequestId = | ||||||
|  |   | RequestId of Cuid | ||||||
|  | with | ||||||
|  |   /// The string representation of the request ID | ||||||
|  |   override x.ToString () = match x with RequestId y -> (string >> sprintf "Requests/%s") y | ||||||
|  |   /// Create a request ID from a string representation | ||||||
|  |   static member fromIdString (y : string) = (Cuid >> RequestId) <| y.Replace("Requests/", "") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// User ID is a string (the "sub" part of the JWT) | ||||||
|  | type UserId = | ||||||
|  |   | UserId of string | ||||||
|  | with | ||||||
|  |   /// The string representation of the user ID | ||||||
|  |   override x.ToString () = match x with UserId y -> y | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// A long integer representing seconds since the epoch | ||||||
|  | type Ticks = | ||||||
|  |   | Ticks of int64 | ||||||
|  | with | ||||||
|  |   /// The int64 (long) representation of ticks | ||||||
|  |   member x.toLong () = match x with Ticks y -> y | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// How frequently a request should reappear after it is marked "Prayed" | ||||||
|  | type Recurrence = | ||||||
|  |   | Immediate | ||||||
|  |   | Hours | ||||||
|  |   | Days | ||||||
|  |   | Weeks | ||||||
|  | with | ||||||
|  |   /// The string reprsentation used in the database and the web app | ||||||
|  |   override x.ToString () = | ||||||
|  |     match x with | ||||||
|  |     | Immediate -> "immediate" | ||||||
|  |     | Hours -> "hours" | ||||||
|  |     | Days -> "days" | ||||||
|  |     | Weeks -> "weeks" | ||||||
|  |   /// Create a recurrence value from a string | ||||||
|  |   static member fromString x = | ||||||
|  |     match x with | ||||||
|  |     | "immediate" -> Immediate | ||||||
|  |     | "hours" -> Hours | ||||||
|  |     | "days" -> Days | ||||||
|  |     | "weeks" -> Weeks | ||||||
|  |     | _ -> invalidOp (sprintf "%s is not a valid recurrence" x) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// History is a record of action taken on a prayer request, including updates to its text | ||||||
|  | [<CLIMutable; NoComparison; NoEquality>] | ||||||
|  | type History = | ||||||
|  |   { /// The time when this history entry was made | ||||||
|  |     asOf   : Ticks | ||||||
|  |     /// The status for this history entry | ||||||
|  |     status : string | ||||||
|  |     /// The text of the update, if applicable | ||||||
|  |     text   : string option | ||||||
|  |     } | ||||||
|  | with | ||||||
|  |   /// An empty history entry | ||||||
|  |   static member empty = | ||||||
|  |     { asOf   = Ticks 0L | ||||||
|  |       status = "" | ||||||
|  |       text   = None | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  | /// Note is a note regarding a prayer request that does not result in an update to its text | ||||||
|  | [<CLIMutable; NoComparison; NoEquality>] | ||||||
|  | type Note = | ||||||
|  |   { /// The time when this note was made | ||||||
|  |     asOf  : Ticks | ||||||
|  |     /// The text of the notes | ||||||
|  |     notes : string | ||||||
|  |     } | ||||||
|  | with | ||||||
|  |   /// An empty note | ||||||
|  |   static member empty = | ||||||
|  |     { asOf  = Ticks 0L | ||||||
|  |       notes = "" | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  | /// Request is the identifying record for a prayer request | ||||||
|  | [<CLIMutable; NoComparison; NoEquality>] | ||||||
|  | type Request = | ||||||
|  |   { /// The ID of the request | ||||||
|  |     Id           : string | ||||||
|  |     /// The time this request was initially entered | ||||||
|  |     enteredOn    : Ticks | ||||||
|  |     /// 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 : Ticks | ||||||
|  |     /// The time at which this request should reappear in the user's journal by recurrence | ||||||
|  |     showAfter    : Ticks | ||||||
|  |     /// The type of recurrence for this request | ||||||
|  |     recurType    : Recurrence | ||||||
|  |     /// How many of the recurrence intervals should occur between appearances in the journal | ||||||
|  |     recurCount   : int16 | ||||||
|  |     /// The history entries for this request | ||||||
|  |     history      : History list | ||||||
|  |     /// The notes for this request | ||||||
|  |     notes        : Note list | ||||||
|  |     } | ||||||
|  | with | ||||||
|  |   /// An empty request | ||||||
|  |   static member empty = | ||||||
|  |     { Id           = "" | ||||||
|  |       enteredOn    = Ticks 0L | ||||||
|  |       userId       = UserId "" | ||||||
|  |       snoozedUntil = Ticks 0L | ||||||
|  |       showAfter    = Ticks 0L | ||||||
|  |       recurType    = Immediate | ||||||
|  |       recurCount   = 0s | ||||||
|  |       history      = [] | ||||||
|  |       notes        = [] | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  | /// JournalRequest is the form of a prayer request returned for the request journal display. It also contains | ||||||
|  | /// properties that may be filled for history and notes | ||||||
|  | [<CLIMutable; NoComparison; NoEquality>] | ||||||
|  | type JournalRequest = | ||||||
|  |   { /// The ID of the request | ||||||
|  |     requestId    : RequestId | ||||||
|  |     /// The ID of the user to whom the request belongs | ||||||
|  |     userId       : UserId | ||||||
|  |     /// The current text of the request | ||||||
|  |     text         : string | ||||||
|  |     /// The last time action was taken on the request | ||||||
|  |     asOf         : Ticks | ||||||
|  |     /// The last status for the request | ||||||
|  |     lastStatus   : string | ||||||
|  |     /// The time that this request should reappear in the user's journal | ||||||
|  |     snoozedUntil : Ticks | ||||||
|  |     /// The time after which this request should reappear in the user's journal by configured recurrence | ||||||
|  |     showAfter    : Ticks | ||||||
|  |     /// The type of recurrence for this request | ||||||
|  |     recurType    : Recurrence | ||||||
|  |     /// 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 | ||||||
|  |     notes        : Note list | ||||||
|  |     } | ||||||
| @ -40,6 +40,7 @@ module Error = | |||||||
| module private Helpers = | module private Helpers = | ||||||
|    |    | ||||||
|   open Microsoft.AspNetCore.Http |   open Microsoft.AspNetCore.Http | ||||||
|  |   open Raven.Client.Documents | ||||||
|   open System.Threading.Tasks |   open System.Threading.Tasks | ||||||
|   open System.Security.Claims |   open System.Security.Claims | ||||||
| 
 | 
 | ||||||
| @ -47,6 +48,10 @@ module private Helpers = | |||||||
|   let db (ctx : HttpContext) = |   let db (ctx : HttpContext) = | ||||||
|     ctx.GetService<AppDbContext> () |     ctx.GetService<AppDbContext> () | ||||||
| 
 | 
 | ||||||
|  |   /// Create a RavenDB session | ||||||
|  |   let session (ctx : HttpContext) = | ||||||
|  |     ctx.GetService<IDocumentStore>().OpenAsyncSession () | ||||||
|  | 
 | ||||||
|   /// Get the user's "sub" claim |   /// Get the user's "sub" claim | ||||||
|   let user (ctx : HttpContext) = |   let user (ctx : HttpContext) = | ||||||
|     ctx.User.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier) |     ctx.User.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier) | ||||||
| @ -54,7 +59,7 @@ module private Helpers = | |||||||
|   /// Get the current user's ID |   /// Get the current user's ID | ||||||
|   //  NOTE: this may raise if you don't run the request through the authorize handler first |   //  NOTE: this may raise if you don't run the request through the authorize handler first | ||||||
|   let userId ctx = |   let userId ctx = | ||||||
|     ((user >> Option.get) ctx).Value |     ((user >> Option.get) ctx).Value |> UserId | ||||||
| 
 | 
 | ||||||
|   /// Return a 201 CREATED response |   /// Return a 201 CREATED response | ||||||
|   let created next ctx = |   let created next ctx = | ||||||
| @ -163,28 +168,28 @@ module Request = | |||||||
|     >=> fun next ctx -> |     >=> fun next ctx -> | ||||||
|       task { |       task { | ||||||
|         let! r     = ctx.BindJsonAsync<Models.Request> () |         let! r     = ctx.BindJsonAsync<Models.Request> () | ||||||
|         let  db    = db ctx |         use  sess  = session ctx | ||||||
|         let  reqId = Cuid.Generate () |         let  reqId = (Cuid.Generate >> Domain.Cuid >> RequestId) () | ||||||
|         let  usrId = userId ctx |         let  usrId = userId ctx | ||||||
|         let  now   = jsNow () |         let  now   = (jsNow >> Ticks) () | ||||||
|         { Request.empty with |         do! sess.AddRequest | ||||||
|             requestId  = reqId |               { Request.empty with | ||||||
|             userId     = usrId |                   Id         = string reqId | ||||||
|             enteredOn  = now |                   userId     = usrId | ||||||
|             showAfter  = now |                   enteredOn  = now | ||||||
|             recurType  = r.recurType |                   showAfter  = now | ||||||
|             recurCount = r.recurCount |                   recurType  = Recurrence.fromString r.recurType | ||||||
|           } |                   recurCount = r.recurCount | ||||||
|         |> db.AddEntry |                   history    = [ | ||||||
|         { History.empty with |                     { History.empty with | ||||||
|             requestId = reqId |                         asOf   = now | ||||||
|             asOf      = now |                         status = "Created" | ||||||
|             status    = "Created" |                         text   = Some r.requestText | ||||||
|             text      = Some r.requestText |                       }       | ||||||
|             } |                     ] | ||||||
|         |> db.AddEntry |                 } | ||||||
|         let! _   = db.SaveChangesAsync () |         do! sess.SaveChangesAsync () | ||||||
|         match! db.TryJournalById reqId usrId with |         match! sess.TryJournalById reqId usrId with | ||||||
|         | Some req -> return! (setStatusCode 201 >=> json req) next ctx |         | Some req -> return! (setStatusCode 201 >=> json req) next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|         } |         } | ||||||
| @ -194,23 +199,23 @@ module Request = | |||||||
|     authorize |     authorize | ||||||
|     >=> fun next ctx -> |     >=> fun next ctx -> | ||||||
|       task { |       task { | ||||||
|         let db = db ctx |         use sess  = session ctx | ||||||
|         match! db.TryRequestById reqId (userId ctx) with |         let reqId = (Domain.Cuid >> RequestId) reqId | ||||||
|  |         match! sess.TryRequestById reqId (userId ctx) with | ||||||
|         | Some req -> |         | Some req -> | ||||||
|             let! hist = ctx.BindJsonAsync<Models.HistoryEntry> () |             let! hist = ctx.BindJsonAsync<Models.HistoryEntry> () | ||||||
|             let  now  = jsNow () |             let  now  = (jsNow >> Ticks) () | ||||||
|             { History.empty with |             { History.empty with | ||||||
|                 requestId = reqId |                 asOf   = now | ||||||
|                 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 |             |> sess.AddHistory reqId | ||||||
|             match hist.status with |             match hist.status with | ||||||
|             | "Prayed" -> |             | "Prayed" -> | ||||||
|                 db.UpdateEntry { req with showAfter = now + (recurrence.[req.recurType] * int64 req.recurCount) } |                 sess.UpdateEntry { req with showAfter = now + (recurrence.[req.recurType] * int64 req.recurCount) } | ||||||
|             | _ -> () |             | _ -> () | ||||||
|             let! _ = db.SaveChangesAsync () |             do! sess.SaveChangesAsync () | ||||||
|             return! created next ctx |             return! created next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|         } |         } | ||||||
| @ -225,9 +230,8 @@ module Request = | |||||||
|         | Some _ -> |         | Some _ -> | ||||||
|             let! notes = ctx.BindJsonAsync<Models.NoteEntry> () |             let! notes = ctx.BindJsonAsync<Models.NoteEntry> () | ||||||
|             { Note.empty with |             { Note.empty with | ||||||
|                 requestId = reqId |                 asOf  = (jsNow >> Ticks) () | ||||||
|                 asOf      = jsNow () |                 notes = notes.notes | ||||||
|                 notes     = notes.notes |  | ||||||
|               } |               } | ||||||
|             |> db.AddEntry |             |> db.AddEntry | ||||||
|             let! _ = db.SaveChangesAsync () |             let! _ = db.SaveChangesAsync () | ||||||
| @ -248,7 +252,8 @@ module Request = | |||||||
|     authorize |     authorize | ||||||
|     >=> fun next ctx -> |     >=> fun next ctx -> | ||||||
|       task { |       task { | ||||||
|         match! (db ctx).TryJournalById reqId (userId ctx) with |         use sess = session ctx | ||||||
|  |         match! sess.TryJournalById reqId (userId ctx) with | ||||||
|         | Some req -> return! json req next ctx |         | Some req -> return! json req next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|         } |         } | ||||||
| @ -258,7 +263,8 @@ module Request = | |||||||
|     authorize |     authorize | ||||||
|     >=> fun next ctx -> |     >=> fun next ctx -> | ||||||
|       task { |       task { | ||||||
|         match! (db ctx).TryFullRequestById reqId (userId ctx) with |         use sess = session ctx | ||||||
|  |         match! sess.TryFullRequestById reqId (userId ctx) with | ||||||
|         | Some req -> return! json req next ctx |         | Some req -> return! json req next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|         } |         } | ||||||
| @ -281,7 +287,7 @@ module Request = | |||||||
|         match! db.TryRequestById reqId (userId ctx) with |         match! db.TryRequestById reqId (userId ctx) with | ||||||
|         | Some req -> |         | Some req -> | ||||||
|             let! show = ctx.BindJsonAsync<Models.Show> () |             let! show = ctx.BindJsonAsync<Models.Show> () | ||||||
|             { req with showAfter = show.showAfter } |             { req with showAfter = Ticks show.showAfter } | ||||||
|             |> db.UpdateEntry |             |> db.UpdateEntry | ||||||
|             let! _ = db.SaveChangesAsync () |             let! _ = db.SaveChangesAsync () | ||||||
|             return! setStatusCode 204 next ctx |             return! setStatusCode 204 next ctx | ||||||
| @ -297,7 +303,7 @@ module Request = | |||||||
|         match! db.TryRequestById reqId (userId ctx) with |         match! db.TryRequestById reqId (userId ctx) with | ||||||
|         | Some req -> |         | Some req -> | ||||||
|             let! until = ctx.BindJsonAsync<Models.SnoozeUntil> () |             let! until = ctx.BindJsonAsync<Models.SnoozeUntil> () | ||||||
|             { req with snoozedUntil = until.until; showAfter = until.until } |             { req with snoozedUntil = Ticks until.until; showAfter = Ticks until.until } | ||||||
|             |> db.UpdateEntry |             |> db.UpdateEntry | ||||||
|             let! _ = db.SaveChangesAsync () |             let! _ = db.SaveChangesAsync () | ||||||
|             return! setStatusCode 204 next ctx |             return! setStatusCode 204 next ctx | ||||||
| @ -313,7 +319,7 @@ module Request = | |||||||
|         match! db.TryRequestById reqId (userId ctx) with |         match! db.TryRequestById reqId (userId ctx) with | ||||||
|         | Some req -> |         | Some req -> | ||||||
|             let! recur = ctx.BindJsonAsync<Models.Recurrence> () |             let! recur = ctx.BindJsonAsync<Models.Recurrence> () | ||||||
|             { req with recurType = recur.recurType; recurCount = recur.recurCount } |             { req with recurType = Recurrence.fromString recur.recurType; recurCount = recur.recurCount } | ||||||
|             |> db.UpdateEntry |             |> db.UpdateEntry | ||||||
|             let! _ = db.SaveChangesAsync () |             let! _ = db.SaveChangesAsync () | ||||||
|             return! setStatusCode 204 next ctx |             return! setStatusCode 204 next ctx | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|  |     <Compile Include="Domain.fs" /> | ||||||
|     <Compile Include="Data.fs" /> |     <Compile Include="Data.fs" /> | ||||||
|     <Compile Include="Handlers.fs" /> |     <Compile Include="Handlers.fs" /> | ||||||
|     <Compile Include="Program.fs" /> |     <Compile Include="Program.fs" /> | ||||||
| @ -20,6 +21,7 @@ | |||||||
|     <PackageReference Include="Microsoft.FSharpLu.Json" Version="0.10.29" /> |     <PackageReference Include="Microsoft.FSharpLu.Json" Version="0.10.29" /> | ||||||
|     <PackageReference Include="NCuid.NetCore" Version="1.0.1" /> |     <PackageReference Include="NCuid.NetCore" Version="1.0.1" /> | ||||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.2.0" /> |     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.2.0" /> | ||||||
|  |     <PackageReference Include="RavenDb.Client" Version="4.2.1" /> | ||||||
|     <PackageReference Include="TaskBuilder.fs" Version="2.1.0" /> |     <PackageReference Include="TaskBuilder.fs" Version="2.1.0" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
| @ -31,8 +33,4 @@ | |||||||
|     <Folder Include="wwwroot\" /> |     <Folder Include="wwwroot\" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
|   <ItemGroup> |  | ||||||
|     <ProjectReference Include="..\MyPrayerJournal.Domain\MyPrayerJournal.Domain.fsproj" /> |  | ||||||
|   </ItemGroup> |  | ||||||
| 
 |  | ||||||
| </Project> | </Project> | ||||||
|  | |||||||
| @ -1,114 +0,0 @@ | |||||||
| [<AutoOpen>] |  | ||||||
| /// Entities for use in the data model for myPrayerJournal |  | ||||||
| module MyPrayerJournal.Entities |  | ||||||
| 
 |  | ||||||
| open System.Collections.Generic |  | ||||||
| 
 |  | ||||||
| /// Type alias for a Collision-resistant Unique IDentifier |  | ||||||
| type Cuid = string |  | ||||||
| 
 |  | ||||||
| /// Request ID is a CUID |  | ||||||
| type RequestId = Cuid |  | ||||||
| 
 |  | ||||||
| /// User ID is a string (the "sub" part of the JWT) |  | ||||||
| type UserId = string |  | ||||||
| 
 |  | ||||||
| /// History is a record of action taken on a prayer request, including updates to its text |  | ||||||
| type [<CLIMutable; NoComparison; NoEquality>] History = |  | ||||||
|   { /// The ID of the request to which this history entry applies |  | ||||||
|     requestId : RequestId |  | ||||||
|     /// The time when this history entry was made |  | ||||||
|     asOf      : int64 |  | ||||||
|     /// The status for this history entry |  | ||||||
|     status    : string |  | ||||||
|     /// The text of the update, if applicable |  | ||||||
|     text      : string option |  | ||||||
|     } |  | ||||||
| with |  | ||||||
|   /// An empty history entry |  | ||||||
|   static member empty = |  | ||||||
|     { requestId = "" |  | ||||||
|       asOf      = 0L |  | ||||||
|       status    = "" |  | ||||||
|       text      = None |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
| /// Note is a note regarding a prayer request that does not result in an update to its text |  | ||||||
| and [<CLIMutable; NoComparison; NoEquality>] Note = |  | ||||||
|   { /// The ID of the request to which this note applies |  | ||||||
|     requestId : RequestId |  | ||||||
|     /// The time when this note was made |  | ||||||
|     asOf      : int64 |  | ||||||
|     /// The text of the notes |  | ||||||
|     notes     : string |  | ||||||
|     } |  | ||||||
| with |  | ||||||
|   /// An empty note |  | ||||||
|   static member empty = |  | ||||||
|     { requestId = "" |  | ||||||
|       asOf      = 0L |  | ||||||
|       notes     = "" |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
| /// Request is the identifying record for a prayer request |  | ||||||
| and [<CLIMutable; NoComparison; NoEquality>] Request = |  | ||||||
|   { /// The ID of the request |  | ||||||
|     requestId    : 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       : string |  | ||||||
|     /// 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<History> |  | ||||||
|     /// The notes for this request |  | ||||||
|     notes        : ICollection<Note> |  | ||||||
|     } |  | ||||||
| with |  | ||||||
|   /// An empty request |  | ||||||
|   static member empty = |  | ||||||
|     { requestId    = "" |  | ||||||
|       enteredOn    = 0L |  | ||||||
|       userId       = "" |  | ||||||
|       snoozedUntil = 0L |  | ||||||
|       showAfter    = 0L |  | ||||||
|       recurType    = "immediate" |  | ||||||
|       recurCount   = 0s |  | ||||||
|       history      = List<History> () |  | ||||||
|       notes        = List<Note> () |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
| /// JournalRequest is the form of a prayer request returned for the request journal display. It also contains |  | ||||||
| /// properties that may be filled for history and notes |  | ||||||
| [<CLIMutable; NoComparison; NoEquality>] |  | ||||||
| type JournalRequest = |  | ||||||
|   { /// The ID of the request |  | ||||||
|     requestId    : RequestId |  | ||||||
|     /// The ID of the user to whom the request belongs |  | ||||||
|     userId       : string |  | ||||||
|     /// The current text of the request |  | ||||||
|     text         : string |  | ||||||
|     /// The last time action was taken on the request |  | ||||||
|     asOf         : int64 |  | ||||||
|     /// The last status for the request |  | ||||||
|     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 |  | ||||||
|     notes        : Note list |  | ||||||
|     } |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| <Project Sdk="Microsoft.NET.Sdk"> |  | ||||||
| 
 |  | ||||||
|   <PropertyGroup> |  | ||||||
|     <TargetFramework>netstandard2.0</TargetFramework> |  | ||||||
|   </PropertyGroup> |  | ||||||
| 
 |  | ||||||
|   <ItemGroup> |  | ||||||
|     <Compile Include="Entities.fs" /> |  | ||||||
|   </ItemGroup> |  | ||||||
| 
 |  | ||||||
| </Project> |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user