WIP on doc queries (#55)
This commit is contained in:
		
							parent
							
								
									bade89dd37
								
							
						
					
					
						commit
						14b0a58d98
					
				| @ -1,9 +1,5 @@ | ||||
| namespace PrayerTracker.Data | ||||
| 
 | ||||
| open System | ||||
| open NodaTime | ||||
| open PrayerTracker.Entities | ||||
| 
 | ||||
| /// Table names | ||||
| [<RequireQualifiedAccess>] | ||||
| module Table = | ||||
| @ -29,6 +25,10 @@ module Table = | ||||
|     let User = "pt_user" | ||||
| 
 | ||||
| 
 | ||||
| open System | ||||
| open NodaTime | ||||
| open PrayerTracker.Entities | ||||
| 
 | ||||
| /// JSON serialization customizations | ||||
| [<RequireQualifiedAccess>] | ||||
| module Json = | ||||
| @ -36,12 +36,10 @@ 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) = | ||||
|     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) | ||||
|         override _.Read(reader, _, _) = wrap (reader.GetString()) | ||||
|         override _.Write(writer, value, _) = writer.WriteStringValue(unwrap value) | ||||
| 
 | ||||
|     open System.Text.Json | ||||
|     open NodaTime.Serialization.SystemTextJson | ||||
| @ -49,6 +47,7 @@ module Json = | ||||
|     /// JSON serializer options to support the target domain | ||||
|     let options = | ||||
|         let opts = JsonSerializerOptions() | ||||
| 
 | ||||
|         [ WrappedJsonConverter<AsOfDateDisplay>(AsOfDateDisplay.Parse, string) :> JsonConverter | ||||
|           WrappedJsonConverter<EmailFormat>(EmailFormat.Parse, string) | ||||
|           WrappedJsonConverter<Expiration>(Expiration.Parse, string) | ||||
| @ -62,11 +61,13 @@ module Json = | ||||
|           WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string) | ||||
|           JsonFSharpConverter() ] | ||||
|         |> List.iter opts.Converters.Add | ||||
| 
 | ||||
|         let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb | ||||
|         opts.PropertyNamingPolicy   <- JsonNamingPolicy.CamelCase | ||||
|         opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase | ||||
|         opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull | ||||
|         opts | ||||
| 
 | ||||
| 
 | ||||
| open BitBadger.Documents | ||||
| open BitBadger.Documents.Sqlite | ||||
| 
 | ||||
| @ -77,106 +78,187 @@ module Connection = | ||||
|     open System.Text.Json | ||||
| 
 | ||||
|     /// Ensure tables and indexes are defined | ||||
|     let setUp () = backgroundTask { | ||||
|         Configuration.useIdField "id" | ||||
|         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) | ||||
|             } | ||||
|     let setUp () = | ||||
|         backgroundTask { | ||||
|             Configuration.useIdField "id" | ||||
| 
 | ||||
|         let! tables = Custom.list<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0) | ||||
|         if not (List.contains Table.Church tables) then | ||||
|             do! Definition.ensureTable Table.Church | ||||
|         if not (List.contains Table.Group tables) then | ||||
|             do! Definition.ensureTable Table.Group | ||||
|             do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ] | ||||
|         if not (List.contains Table.Member tables) then | ||||
|             do! Definition.ensureTable Table.Member | ||||
|             do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ] | ||||
|         if not (List.contains Table.Request tables) then | ||||
|             do! Definition.ensureTable Table.Request | ||||
|             do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ] | ||||
|         if not (List.contains Table.User tables) then | ||||
|             do! Definition.ensureTable Table.User | ||||
|             do! Definition.ensureFieldIndex Table.User "email" [ "email" ] | ||||
|     } | ||||
|             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) } | ||||
| 
 | ||||
|             let! tables = Custom.list<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0) | ||||
| 
 | ||||
|             if not (List.contains Table.Church tables) then | ||||
|                 do! Definition.ensureTable Table.Church | ||||
| 
 | ||||
|             if not (List.contains Table.Group tables) then | ||||
|                 do! Definition.ensureTable Table.Group | ||||
|                 do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ] | ||||
| 
 | ||||
|             if not (List.contains Table.Member tables) then | ||||
|                 do! Definition.ensureTable Table.Member | ||||
|                 do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ] | ||||
| 
 | ||||
|             if not (List.contains Table.Request tables) then | ||||
|                 do! Definition.ensureTable Table.Request | ||||
|                 do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ] | ||||
| 
 | ||||
|             if not (List.contains Table.User tables) then | ||||
|                 do! Definition.ensureTable Table.User | ||||
|                 do! Definition.ensureFieldIndex Table.User "email" [ "email" ] | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| open Microsoft.Data.Sqlite | ||||
| 
 | ||||
| /// Helper functions for the PostgreSQL data implementation | ||||
| [<AutoOpen>] | ||||
| module private Helpers = | ||||
| 
 | ||||
|     /// Map a row to a Prayer Request instance | ||||
|     let mapToPrayerRequest (row : RowReader) = | ||||
|         {   Id             = PrayerRequestId         (row.uuid "id") | ||||
|             UserId         = UserId                  (row.uuid "user_id") | ||||
|             SmallGroupId   = SmallGroupId            (row.uuid "small_group_id") | ||||
|             EnteredDate    = row.fieldValue<Instant> "entered_date" | ||||
|             UpdatedDate    = row.fieldValue<Instant> "updated_date" | ||||
|             Requestor      = row.stringOrNone        "requestor" | ||||
|             Text           = row.string              "request_text" | ||||
|             NotifyChaplain = row.bool                "notify_chaplain" | ||||
|             RequestType    = PrayerRequestType.Parse (row.string "request_type") | ||||
|             Expiration     = Expiration.Parse        (row.string "expiration") | ||||
|         } | ||||
|     let mapToPrayerRequest (row: RowReader) = | ||||
|         { Id = PrayerRequestId(row.uuid "id") | ||||
|           UserId = UserId(row.uuid "user_id") | ||||
|           SmallGroupId = SmallGroupId(row.uuid "small_group_id") | ||||
|           EnteredDate = row.fieldValue<Instant> "entered_date" | ||||
|           UpdatedDate = row.fieldValue<Instant> "updated_date" | ||||
|           Requestor = row.stringOrNone "requestor" | ||||
|           Text = row.string "request_text" | ||||
|           NotifyChaplain = row.bool "notify_chaplain" | ||||
|           RequestType = PrayerRequestType.Parse(row.string "request_type") | ||||
|           Expiration = Expiration.Parse(row.string "expiration") } | ||||
| 
 | ||||
|     /// Map a row to a Small Group information set | ||||
|     let mapToSmallGroupInfo (row : RowReader) = | ||||
|         {   Id         = Giraffe.ShortGuid.fromGuid (row.uuid "id") | ||||
|             Name       = row.string "group_name" | ||||
|             ChurchName = row.string "church_name" | ||||
|             TimeZoneId = TimeZoneId (row.string "time_zone_id") | ||||
|             IsPublic   = row.bool   "is_public" | ||||
|         } | ||||
| 
 | ||||
| open Npgsql | ||||
| 
 | ||||
| /// Functions to retrieve small group information | ||||
| module SmallGroups = | ||||
| 
 | ||||
|     /// Query to retrieve data for a small group info instance | ||||
|     let private infoQuery = | ||||
|         $"SELECT g.data->>'id' AS id, g.data->>'groupName' AS groupName, c.data->>'churchName' AS churchName, | ||||
|                  g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic | ||||
|             FROM {Table.Group} g | ||||
|                  INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" | ||||
| 
 | ||||
|     /// Query to retrieve data for a small group select list item | ||||
|     let private itemQuery = | ||||
|         $"SELECT g.data->>'groupName' AS groupName, g.data->>'id' AS id, c.data->>'churchName' AS churchName | ||||
|             FROM {Table.Group} g | ||||
|                  INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'" | ||||
| 
 | ||||
|     /// The ORDER BY clause for select list item queries | ||||
|     let private itemOrderBy = "ORDER BY c.data->>'churchName', g.data->>'groupName'" | ||||
| 
 | ||||
|     /// Map a row to a Small Group list item | ||||
|     let mapToSmallGroupItem (row : RowReader) = | ||||
|         Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""" | ||||
|     let private toSmallGroupItem (rdr: SqliteDataReader) = | ||||
|         (rdr.GetOrdinal >> rdr.GetString >> Guid.Parse >> Giraffe.ShortGuid.fromGuid) "id", | ||||
|         $"""{(rdr.GetOrdinal >> rdr.GetString) "churchName"} | {(rdr.GetOrdinal >> rdr.GetString) "groupName"}""" | ||||
| 
 | ||||
|     /// Map a row to a User instance | ||||
|     let mapToUser (row : RowReader) = | ||||
|         {   Id           = UserId     (row.uuid "id") | ||||
|             FirstName    = row.string "first_name" | ||||
|             LastName     = row.string "last_name" | ||||
|             Email        = row.string "email" | ||||
|             IsAdmin      = row.bool   "is_admin" | ||||
|             PasswordHash = row.string "password_hash" | ||||
|             LastSeen     = row.fieldValueOrNone<Instant> "last_seen" | ||||
|             SmallGroups  = [] | ||||
|     /// Get the group IDs for the given church | ||||
|     let internal groupIdsByChurch (churchId: ChurchId) = | ||||
|         backgroundTask { | ||||
|             let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" churchId ] | ||||
|             return groups |> List.map _.Id | ||||
|         } | ||||
| 
 | ||||
|     /// Count the number of small groups for a church | ||||
|     let countByChurch (churchId: ChurchId) = | ||||
|         Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] | ||||
| 
 | ||||
|     /// Delete a small group by its ID | ||||
|     let deleteById (groupId: SmallGroupId) = | ||||
|         backgroundTask { | ||||
|             use conn = Configuration.dbConn () | ||||
|             use! txn = conn.BeginTransactionAsync() | ||||
| 
 | ||||
|             let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
| 
 | ||||
|             for user in users do | ||||
|                 do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} | ||||
| 
 | ||||
|             do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | ||||
|             do! conn.deleteById Table.Group groupId | ||||
| 
 | ||||
|             do! txn.CommitAsync() | ||||
|         } | ||||
| 
 | ||||
|     /// Get information for all small groups | ||||
|     let infoForAll () = | ||||
|         Custom.list $"{infoQuery} ORDER BY g.data->>'groupName'" [] SmallGroupInfo.FromReader | ||||
| 
 | ||||
|     /// Get a list of small group IDs along with a description that includes the church name | ||||
|     let listAll () = | ||||
|         Custom.list $"{itemQuery} {itemOrderBy}" [] toSmallGroupItem | ||||
| 
 | ||||
|     /// Get a list of small group IDs and descriptions for groups with a group password | ||||
|     let listProtected () = | ||||
|         Custom.list | ||||
|             $"{itemQuery} WHERE COALESCE(g.data->'preferences'->>'groupPassword', '') <> '' {itemOrderBy}" | ||||
|             [] | ||||
|             toSmallGroupItem | ||||
| 
 | ||||
|     /// Get a list of small group IDs and descriptions for groups that are public or have a group password | ||||
|     let listPublicAndProtected () = | ||||
|         Custom.list | ||||
|             $"{infoQuery} | ||||
|               WHERE g.data->'preferences'->>'isPublic' = TRUE | ||||
|                  OR COALESCE(g.data->'preferences'->>'groupPassword', '') <> '' | ||||
|               ORDER BY c.data->>'churchName', g.data->>'groupName'" | ||||
|             [] | ||||
|             SmallGroupInfo.FromReader | ||||
| 
 | ||||
|     /// Log on for a small group (includes list preferences) | ||||
|     let logOn (groupId: SmallGroupId) (password: string) = | ||||
|         Find.firstByFields<SmallGroup> | ||||
|             Table.Group | ||||
|             All | ||||
|             [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] | ||||
| 
 | ||||
|     /// Save a small group | ||||
|     let save group = save<SmallGroup> Table.Group group | ||||
| 
 | ||||
|     /// Save a small group's list preferences | ||||
|     let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) = | ||||
|         Patch.byId Table.Group groupId {| Preferences = pref |} | ||||
| 
 | ||||
|     /// Get a small group by its ID (including list preferences) | ||||
|     let tryById groupId = | ||||
|         Find.byId<SmallGroupId, SmallGroup> Table.Group groupId | ||||
| 
 | ||||
| open BitBadger.Documents | ||||
| open Npgsql | ||||
| open Npgsql.FSharp | ||||
| 
 | ||||
| /// Functions to manipulate churches | ||||
| module Churches = | ||||
| 
 | ||||
|     /// Get a list of all churches | ||||
|     let all () = | ||||
|         Find.all<Church> Table.Church | ||||
|     let all () = Find.all<Church> Table.Church | ||||
| 
 | ||||
|     /// Delete a church by its ID | ||||
|     let deleteById (churchId: ChurchId) = backgroundTask { | ||||
|         let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ] | ||||
|         let where   = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" | ||||
|         let! _ = | ||||
|             BitBadger.Documents.Postgres.Configuration.dataSource () | ||||
|             |> Sql.fromDataSource | ||||
|             |> Sql.executeTransactionAsync | ||||
|                 [   $"DELETE FROM pt.prayer_request {where}", idParam | ||||
|                     $"DELETE FROM pt.user_small_group {where}", idParam | ||||
|                     $"DELETE FROM pt.list_preference {where}", idParam | ||||
|                     "DELETE FROM pt.small_group WHERE church_id = @churchId", idParam | ||||
|                     "DELETE FROM pt.church WHERE id = @churchId", idParam ] | ||||
|         () | ||||
|     } | ||||
|     let deleteById churchId = | ||||
|         backgroundTask { | ||||
|             use conn = Configuration.dbConn () | ||||
|             use! txn = conn.BeginTransactionAsync() | ||||
| 
 | ||||
|             let! groupIds = SmallGroups.groupIdsByChurch churchId | ||||
| 
 | ||||
|             do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ] | ||||
| 
 | ||||
|             let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] | ||||
| 
 | ||||
|             for user in users do | ||||
|                 do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except groupIds |} | ||||
| 
 | ||||
|             do! Delete.byFields Table.Group All [ Field.Equal "churchId" churchId ] | ||||
|             do! Delete.byId Table.Church churchId | ||||
|             do! txn.CommitAsync() | ||||
|         } | ||||
| 
 | ||||
|     /// Save a church's information | ||||
|     let save church = | ||||
|         save<Church> Table.Church church | ||||
|     let save church = save<Church> Table.Church church | ||||
| 
 | ||||
|     /// Find a church by its ID | ||||
|     let tryById churchId = | ||||
| @ -191,17 +273,18 @@ module Members = | ||||
|         Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] | ||||
| 
 | ||||
|     /// Delete a small group member by its ID | ||||
|     let deleteById (memberId: MemberId) = | ||||
|         Delete.byId Table.Member memberId | ||||
|     let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId | ||||
| 
 | ||||
|     /// Retrieve all members for a given small group | ||||
|     let forGroup (groupId : SmallGroupId) = | ||||
|     let forGroup (groupId: SmallGroupId) = | ||||
|         Find.byFieldsOrdered<Member> | ||||
|             Table.Member All [ Field.Equal "smallGroupId" groupId ] [ Field.Named "memberName" ] | ||||
|             Table.Member | ||||
|             All | ||||
|             [ Field.Equal "smallGroupId" groupId ] | ||||
|             [ Field.Named "memberName" ] | ||||
| 
 | ||||
|     /// Save a small group member | ||||
|     let save mbr = | ||||
|         save<Member> Table.Member mbr | ||||
|     let save mbr = save<Member> Table.Member mbr | ||||
| 
 | ||||
|     /// Retrieve a small group member by its ID | ||||
|     let tryById memberId = | ||||
| @ -210,20 +293,21 @@ module Members = | ||||
| 
 | ||||
| /// Options to retrieve a list of requests | ||||
| type PrayerRequestOptions = | ||||
|     {   /// The small group for which requests should be retrieved | ||||
|         SmallGroup : SmallGroup | ||||
|     { | ||||
|         /// The small group for which requests should be retrieved | ||||
|         SmallGroup: SmallGroup | ||||
| 
 | ||||
|         /// The clock instance to use for date/time manipulation | ||||
|         Clock : IClock | ||||
|         Clock: IClock | ||||
| 
 | ||||
|         /// The date for which the list is being retrieved | ||||
|         ListDate : LocalDate option | ||||
|         ListDate: LocalDate option | ||||
| 
 | ||||
|         /// Whether only active requests should be retrieved | ||||
|         ActiveOnly : bool | ||||
|         ActiveOnly: bool | ||||
| 
 | ||||
|         /// The page number, for paged lists | ||||
|         PageNumber : int | ||||
|         PageNumber: int | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @ -237,57 +321,66 @@ module PrayerRequests = | ||||
|         | SortByRequestor -> "requestor, updated_date DESC, entered_date DESC" | ||||
| 
 | ||||
|     /// Paginate a prayer request query | ||||
|     let private paginate (pageNbr : int) pageSize = | ||||
|         if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else "" | ||||
|     let private paginate (pageNbr: int) pageSize = | ||||
|         if pageNbr > 0 then | ||||
|             $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" | ||||
|         else | ||||
|             "" | ||||
| 
 | ||||
|     /// Count the number of prayer requests for a church | ||||
|     let countByChurch (churchId : ChurchId) = | ||||
|     let countByChurch (churchId: ChurchId) = | ||||
|         BitBadger.Documents.Postgres.Custom.scalar | ||||
|             "SELECT COUNT(id) AS req_count | ||||
|                FROM pt.prayer_request | ||||
|               WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)" | ||||
|             [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "req_count") | ||||
|             [ "@churchId", Sql.uuid churchId.Value ] | ||||
|             (fun row -> row.int "req_count") | ||||
| 
 | ||||
|     /// Count the number of prayer requests for a small group | ||||
|     let countByGroup (groupId: SmallGroupId) = | ||||
|         Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | ||||
| 
 | ||||
|     /// Delete a prayer request by its ID | ||||
|     let deleteById (reqId: PrayerRequestId) = | ||||
|         Delete.byId Table.Request reqId | ||||
|     let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId | ||||
| 
 | ||||
|     /// Get all (or active) requests for a small group as of now or the specified date | ||||
|     let forGroup (opts : PrayerRequestOptions) = | ||||
|     let forGroup (opts: PrayerRequestOptions) = | ||||
|         let theDate = defaultArg opts.ListDate (opts.SmallGroup.LocalDateNow opts.Clock) | ||||
| 
 | ||||
|         let where, parameters = | ||||
|             if opts.ActiveOnly then | ||||
|                 let asOf = NpgsqlParameter ( | ||||
|                     "@asOf", | ||||
|                     (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) | ||||
|                             - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) | ||||
|                         .ToInstant ()) | ||||
|                 let asOf = | ||||
|                     NpgsqlParameter( | ||||
|                         "@asOf", | ||||
|                         (theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone) | ||||
|                          - Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire) | ||||
|                             .ToInstant() | ||||
|                     ) | ||||
| 
 | ||||
|                 "   AND (   updated_date > @asOf | ||||
|                          OR expiration   = @manual | ||||
|                          OR request_type = @longTerm | ||||
|                          OR request_type = @expecting) | ||||
|                     AND expiration <> @forced", | ||||
|                 [   "@asOf",      Sql.parameter asOf | ||||
|                     "@manual",    Sql.string    (string Manual) | ||||
|                     "@longTerm",  Sql.string    (string LongTermRequest) | ||||
|                     "@expecting", Sql.string    (string Expecting) | ||||
|                     "@forced",    Sql.string    (string Forced) ] | ||||
|             else "", [] | ||||
|                 [ "@asOf", Sql.parameter asOf | ||||
|                   "@manual", Sql.string (string Manual) | ||||
|                   "@longTerm", Sql.string (string LongTermRequest) | ||||
|                   "@expecting", Sql.string (string Expecting) | ||||
|                   "@forced", Sql.string (string Forced) ] | ||||
|             else | ||||
|                 "", [] | ||||
| 
 | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             $"SELECT * | ||||
|                 FROM pt.prayer_request | ||||
|                WHERE small_group_id = @groupId {where} | ||||
|                ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort} | ||||
|                {paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}" | ||||
|             (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest | ||||
|             (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) | ||||
|             mapToPrayerRequest | ||||
| 
 | ||||
|     /// Save a prayer request | ||||
|     let save req = | ||||
|         save<PrayerRequest> Table.Request req | ||||
|     let save req = save<PrayerRequest> Table.Request req | ||||
| 
 | ||||
|     /// Search prayer requests for the given term | ||||
|     let searchForGroup group searchTerm pageNbr = | ||||
| @ -297,7 +390,9 @@ module PrayerRequests = | ||||
|               SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search | ||||
|               ORDER BY {orderBy group.Preferences.RequestSort} | ||||
|               {paginate pageNbr group.Preferences.PageSize}" | ||||
|             [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest | ||||
|             [ "@groupId", Sql.uuid group.Id.Value | ||||
|               "@search", Sql.string $"%%%s{searchTerm}%%" ] | ||||
|             mapToPrayerRequest | ||||
| 
 | ||||
|     /// Retrieve a prayer request by its ID | ||||
|     let tryById reqId = | ||||
| @ -306,92 +401,15 @@ module PrayerRequests = | ||||
|     /// Update the expiration for the given prayer request | ||||
|     let updateExpiration (req: PrayerRequest) withTime = | ||||
|         if withTime then | ||||
|             Patch.byId Table.Request req.Id {| UpdatedDate = req.UpdatedDate; Expiration = req.Expiration |} | ||||
|             Patch.byId | ||||
|                 Table.Request | ||||
|                 req.Id | ||||
|                 {| UpdatedDate = req.UpdatedDate | ||||
|                    Expiration = req.Expiration |} | ||||
|         else | ||||
|             Patch.byId Table.Request req.Id {| Expiration = req.Expiration |} | ||||
| 
 | ||||
| 
 | ||||
| /// Functions to retrieve small group information | ||||
| module SmallGroups = | ||||
| 
 | ||||
|     /// Count the number of small groups for a church | ||||
|     let countByChurch (churchId: ChurchId) = | ||||
|         Count.byFields Table.Group All [ Field.Equal "churchId" churchId ] | ||||
| 
 | ||||
|     /// Delete a small group by its ID | ||||
|     let deleteById (groupId: SmallGroupId) = backgroundTask { | ||||
|         use conn = Configuration.dbConn () | ||||
|         use txn  = conn.BeginTransaction() | ||||
|          | ||||
|         let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
|         for user in users do | ||||
|             do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |} | ||||
|         do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | ||||
|         do! conn.deleteById Table.Group groupId | ||||
|          | ||||
|         do! txn.CommitAsync() | ||||
|     } | ||||
| 
 | ||||
|     /// Get information for all small groups | ||||
|     let infoForAll () = | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             "SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public | ||||
|                FROM pt.small_group sg | ||||
|                     INNER JOIN pt.church c ON c.id = sg.church_id | ||||
|                     INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id | ||||
|               ORDER BY sg.group_name" | ||||
|             [] mapToSmallGroupInfo | ||||
| 
 | ||||
|     /// Get a list of small group IDs along with a description that includes the church name | ||||
|     let listAll () = | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             "SELECT g.group_name, g.id, c.church_name | ||||
|                FROM pt.small_group g | ||||
|                     INNER JOIN pt.church c ON c.id = g.church_id | ||||
|               ORDER BY c.church_name, g.group_name" | ||||
|             [] mapToSmallGroupItem | ||||
| 
 | ||||
|     /// Get a list of small group IDs and descriptions for groups with a group password | ||||
|     let listProtected () = | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             "SELECT g.group_name, g.id, c.church_name, lp.is_public | ||||
|                FROM pt.small_group g | ||||
|                     INNER JOIN pt.church           c ON c.id = g.church_id | ||||
|                     INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id | ||||
|               WHERE COALESCE(lp.group_password, '') <> '' | ||||
|               ORDER BY c.church_name, g.group_name" | ||||
|             [] mapToSmallGroupItem | ||||
| 
 | ||||
|     /// Get a list of small group IDs and descriptions for groups that are public or have a group password | ||||
|     let listPublicAndProtected () = | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             "SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public | ||||
|                FROM pt.small_group g | ||||
|                     INNER JOIN pt.church           c ON c.id = g.church_id | ||||
|                     INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id | ||||
|               WHERE lp.is_public = TRUE | ||||
|                  OR COALESCE(lp.group_password, '') <> '' | ||||
|               ORDER BY c.church_name, g.group_name" | ||||
|             [] mapToSmallGroupInfo | ||||
| 
 | ||||
|     /// Log on for a small group (includes list preferences) | ||||
|     let logOn (groupId: SmallGroupId) (password: string) = | ||||
|         Find.firstByFields<SmallGroup> | ||||
|             Table.Group All [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] | ||||
| 
 | ||||
|     /// Save a small group | ||||
|     let save group = | ||||
|         save<SmallGroup> Table.Group group | ||||
| 
 | ||||
|     /// Save a small group's list preferences | ||||
|     let savePreferences (pref: ListPreferences) = | ||||
|         Patch.byId Table.Group pref.SmallGroupId {| Preferences = pref |} | ||||
| 
 | ||||
|     /// Get a small group by its ID (including list preferences) | ||||
|     let tryById groupId = | ||||
|         Find.byId<SmallGroupId, SmallGroup> Table.Group groupId | ||||
| 
 | ||||
| 
 | ||||
| /// Functions to manipulate users | ||||
| module Users = | ||||
| 
 | ||||
| @ -400,44 +418,37 @@ module Users = | ||||
|         Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ] | ||||
| 
 | ||||
|     /// Count the number of users for a church | ||||
|     let countByChurch (churchId : ChurchId) = | ||||
|         BitBadger.Documents.Postgres.Custom.scalar | ||||
|             "SELECT COUNT(u.id) AS user_count | ||||
|                FROM pt.pt_user u | ||||
|               WHERE EXISTS ( | ||||
|                     SELECT 1 | ||||
|                       FROM pt.user_small_group usg | ||||
|                            INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id | ||||
|                      WHERE usg.user_id = u.id | ||||
|                        AND sg.church_id = @churchId)" | ||||
|             [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count") | ||||
|     let countByChurch churchId = | ||||
|         backgroundTask { | ||||
|             let! groupIds = SmallGroups.groupIdsByChurch churchId | ||||
|             return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ] | ||||
|         } | ||||
| 
 | ||||
|     /// Count the number of users for a small group | ||||
|     let countByGroup (groupId: SmallGroupId) = | ||||
|         Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
| 
 | ||||
|     /// Delete a user by its database ID | ||||
|     let deleteById (userId: UserId) = | ||||
|         Delete.byId Table.User userId | ||||
|     let deleteById (userId: UserId) = Delete.byId Table.User userId | ||||
| 
 | ||||
|     /// Get a list of users authorized to administer the given small group | ||||
|     let listByGroupId (groupId : SmallGroupId) = | ||||
|         BitBadger.Documents.Postgres.Custom.list | ||||
|             "SELECT u.* | ||||
|                FROM pt.pt_user u | ||||
|                     INNER JOIN pt.user_small_group usg ON usg.user_id = u.id | ||||
|               WHERE usg.small_group_id = @groupId | ||||
|               ORDER BY u.last_name, u.first_name" | ||||
|             [ "@groupId", Sql.uuid groupId.Value ] mapToUser | ||||
|     let listByGroupId (groupId: SmallGroupId) = | ||||
|         Find.byFieldsOrdered<User> | ||||
|             Table.User | ||||
|             All | ||||
|             [ Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
|             [ Field.Named "lastName"; Field.Named "firstName" ] | ||||
| 
 | ||||
|     /// Save a user's information | ||||
|     let save user = | ||||
|         save<User> Table.User user | ||||
|     let save user = save<User> Table.User user | ||||
| 
 | ||||
|     /// Find a user by its e-mail address and authorized small group | ||||
|     let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) = | ||||
|         Find.firstByFields<User> | ||||
|             Table.User All [ Field.Equal "email" email; Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
|             Table.User | ||||
|             All | ||||
|             [ Field.Equal "email" email | ||||
|               Field.InArray "smallGroups" Table.User [ groupId ] ] | ||||
| 
 | ||||
|     /// Find a user by their database ID | ||||
|     let tryById userId = | ||||
|  | ||||
| @ -209,6 +209,8 @@ type UserId = | ||||
| 
 | ||||
| (*-- SPECIFIC VIEW TYPES --*) | ||||
| 
 | ||||
| open Microsoft.Data.Sqlite | ||||
| 
 | ||||
| /// Statistics for churches | ||||
| [<NoComparison; NoEquality>] | ||||
| type ChurchStats = | ||||
| @ -225,7 +227,7 @@ type ChurchStats = | ||||
| 
 | ||||
| 
 | ||||
| /// Information needed to display the public/protected request list and small group maintenance pages | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type SmallGroupInfo = | ||||
|     { | ||||
|         /// The ID of the small group | ||||
| @ -244,12 +246,21 @@ type SmallGroupInfo = | ||||
|         IsPublic: bool | ||||
|     } | ||||
|      | ||||
|     /// Map a row to a Small Group information set | ||||
|     static member FromReader (rdr: SqliteDataReader) = | ||||
|         { Id = Giraffe.ShortGuid.fromGuid ((rdr.GetOrdinal >> rdr.GetString >> Guid.Parse) "id") | ||||
|           Name = (rdr.GetOrdinal >> rdr.GetString) "groupName" | ||||
|           ChurchName = (rdr.GetOrdinal >> rdr.GetString) "churchName" | ||||
|           TimeZoneId = (rdr.GetOrdinal >> rdr.GetString >> TimeZoneId) "timeZoneId" | ||||
|           IsPublic = (rdr.GetOrdinal >> rdr.GetBoolean) "isPublic" } | ||||
| 
 | ||||
| 
 | ||||
| (*-- ENTITIES --*) | ||||
| 
 | ||||
| open NodaTime | ||||
| 
 | ||||
| /// This represents a church | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type Church = | ||||
|     { | ||||
|         /// The ID of this church | ||||
| @ -286,9 +297,6 @@ type Church = | ||||
| [<NoComparison; NoEquality>] | ||||
| type ListPreferences = | ||||
|     { | ||||
|         /// The Id of the small group to which these preferences belong | ||||
|         SmallGroupId: SmallGroupId | ||||
| 
 | ||||
|         /// The days after which regular requests expire | ||||
|         DaysToExpire: int | ||||
| 
 | ||||
| @ -350,8 +358,7 @@ type ListPreferences = | ||||
| 
 | ||||
|     /// A set of preferences with their default values | ||||
|     static member Empty = | ||||
|         { SmallGroupId = SmallGroupId Guid.Empty | ||||
|           DaysToExpire = 14 | ||||
|         { DaysToExpire = 14 | ||||
|           DaysToKeepNew = 7 | ||||
|           LongTermUpdateWeeks = 4 | ||||
|           EmailFromName = "PrayerTracker" | ||||
| @ -371,7 +378,7 @@ type ListPreferences = | ||||
| 
 | ||||
| 
 | ||||
| /// A member of a small group | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type Member = | ||||
|     { | ||||
|         /// The ID of the small group member | ||||
| @ -400,7 +407,7 @@ type Member = | ||||
| 
 | ||||
| 
 | ||||
| /// This represents a small group (Sunday School class, Bible study group, etc.) | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type SmallGroup = | ||||
|     { | ||||
|         /// The ID of this small group | ||||
| @ -444,7 +451,7 @@ type SmallGroup = | ||||
| 
 | ||||
| 
 | ||||
| /// This represents a single prayer request | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type PrayerRequest = | ||||
|     { | ||||
|         /// The ID of this request | ||||
| @ -515,7 +522,7 @@ type PrayerRequest = | ||||
| 
 | ||||
| 
 | ||||
| /// This represents a user of PrayerTracker | ||||
| [<NoComparison; NoEquality>] | ||||
| [<CLIMutable; NoComparison; NoEquality>] | ||||
| type User = | ||||
|     { | ||||
|         /// The ID of this user | ||||
| @ -556,20 +563,3 @@ type User = | ||||
|           PasswordHash = "" | ||||
|           LastSeen = None | ||||
|           SmallGroups = [] } | ||||
| 
 | ||||
| 
 | ||||
| /// Cross-reference between user and small group | ||||
| [<NoComparison; NoEquality>] | ||||
| type UserSmallGroup = | ||||
|     { | ||||
|         /// The Id of the user who has access to the small group | ||||
|         UserId: UserId | ||||
| 
 | ||||
|         /// The Id of the small group to which the user has access | ||||
|         SmallGroupId: SmallGroupId | ||||
|     } | ||||
| 
 | ||||
|     /// An empty user/small group xref | ||||
|     static member Empty = | ||||
|         { UserId = UserId Guid.Empty | ||||
|           SmallGroupId = SmallGroupId Guid.Empty } | ||||
|  | ||||
| @ -39,8 +39,7 @@ module PgMappings = | ||||
|           ChurchId    = ChurchId     (row.uuid "church_id") | ||||
|           Name        = row.string   "group_name" | ||||
|           Preferences = | ||||
|               { SmallGroupId        = SmallGroupId (row.uuid "small_group_id") | ||||
|                 DaysToKeepNew       = row.int      "days_to_keep_new" | ||||
|               { DaysToKeepNew       = row.int      "days_to_keep_new" | ||||
|                 DaysToExpire        = row.int      "days_to_expire" | ||||
|                 LongTermUpdateWeeks = row.int      "long_term_update_weeks" | ||||
|                 EmailFromName       = row.string   "email_from_name" | ||||
|  | ||||
| @ -12,7 +12,7 @@ let private findStats churchId = task { | ||||
|     let! groups   = SmallGroups.countByChurch    churchId | ||||
|     let! requests = PrayerRequests.countByChurch churchId | ||||
|     let! users    = Users.countByChurch          churchId | ||||
|     return shortGuid churchId.Value, { SmallGroups = int groups; PrayerRequests = requests; Users = users } | ||||
|     return shortGuid churchId.Value, { SmallGroups = int groups; PrayerRequests = requests; Users = int users } | ||||
| } | ||||
| 
 | ||||
| // POST /church/[church-id]/delete | ||||
|  | ||||
| @ -230,7 +230,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> | ||||
|         match! SmallGroups.tryById group.Id with | ||||
|         | Some group -> | ||||
|             let pref = model.PopulatePreferences group.Preferences | ||||
|             do! SmallGroups.savePreferences pref | ||||
|             do! SmallGroups.savePreferences group.Id pref | ||||
|             // Refresh session instance | ||||
|             ctx.Session.CurrentGroup <- Some { group with Preferences = pref } | ||||
|             addInfo ctx ctx.Strings["Group preferences updated successfully"] | ||||
|  | ||||
| @ -121,7 +121,6 @@ let listPreferencesTests = | ||||
|         } | ||||
|         test "Empty is as expected" { | ||||
|             let mt = ListPreferences.Empty | ||||
|             Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" | ||||
|             Expect.equal mt.DaysToExpire 14 "The default days to expire should have been 14" | ||||
|             Expect.equal mt.DaysToKeepNew 7 "The default days to keep new should have been 7" | ||||
|             Expect.equal mt.LongTermUpdateWeeks 4 "The default long term update weeks should have been 4" | ||||
| @ -367,13 +366,3 @@ let userTests = | ||||
|             Expect.equal user.Name "Unit Test" "The full name should be the first and last, separated by a space" | ||||
|         } | ||||
|     ] | ||||
| 
 | ||||
| [<Tests>] | ||||
| let userSmallGroupTests = | ||||
|     testList "UserSmallGroup" [ | ||||
|         test "Empty is as expected" { | ||||
|             let mt = UserSmallGroup.Empty | ||||
|             Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" | ||||
|             Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" | ||||
|         } | ||||
|     ] | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user