From 14b0a58d981ce4dae6bd9f788646d08750178f70 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 31 Jan 2025 17:24:12 -0500 Subject: [PATCH] WIP on doc queries (#55) --- src/Data/Access.fs | 473 +++++++++++++------------ src/Data/Entities.fs | 46 +-- src/PrayerTracker.MigrateV9/Program.fs | 3 +- src/PrayerTracker/Church.fs | 2 +- src/PrayerTracker/SmallGroup.fs | 2 +- src/Tests/Data/EntitiesTests.fs | 11 - 6 files changed, 263 insertions(+), 274 deletions(-) diff --git a/src/Data/Access.fs b/src/Data/Access.fs index 912bb01..043ab09 100644 --- a/src/Data/Access.fs +++ b/src/Data/Access.fs @@ -1,9 +1,5 @@ namespace PrayerTracker.Data -open System -open NodaTime -open PrayerTracker.Entities - /// Table names [] module Table = @@ -29,6 +25,10 @@ module Table = let User = "pt_user" +open System +open NodaTime +open PrayerTracker.Entities + /// JSON serialization customizations [] 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.Parse, string) :> JsonConverter WrappedJsonConverter(EmailFormat.Parse, string) WrappedJsonConverter(Expiration.Parse, string) @@ -62,121 +61,204 @@ module Json = WrappedJsonConverter(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 /// Establish the required data environment [] 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! tables = Custom.list "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 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! tables = Custom.list "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 [] 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 "entered_date" - UpdatedDate = row.fieldValue "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 "entered_date" + UpdatedDate = row.fieldValue "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 "last_seen" - SmallGroups = [] + /// Get the group IDs for the given church + let internal groupIdsByChurch (churchId: ChurchId) = + backgroundTask { + let! groups = Find.byFields 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 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 + Table.Group + All + [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] + + /// Save a small group + let save group = save 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 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 Table.Church + let all () = Find.all 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 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 Table.Church church + let save church = save 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 - 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 Table.Member mbr + let save mbr = save 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 Table.Request req + let save req = save 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 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 - Table.Group All [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ] - - /// Save a small group - let save group = - save 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 Table.Group groupId - - /// Functions to manipulate users module Users = @@ -400,44 +418,37 @@ module Users = Find.allOrdered 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 + Table.User + All + [ Field.InArray "smallGroups" Table.User [ groupId ] ] + [ Field.Named "lastName"; Field.Named "firstName" ] /// Save a user's information - let save user = - save Table.User user + let save user = save Table.User user /// Find a user by its e-mail address and authorized small group let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) = Find.firstByFields - 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 = diff --git a/src/Data/Entities.fs b/src/Data/Entities.fs index ed9c4ac..b677c56 100644 --- a/src/Data/Entities.fs +++ b/src/Data/Entities.fs @@ -209,6 +209,8 @@ type UserId = (*-- SPECIFIC VIEW TYPES --*) +open Microsoft.Data.Sqlite + /// Statistics for churches [] type ChurchStats = @@ -225,7 +227,7 @@ type ChurchStats = /// Information needed to display the public/protected request list and small group maintenance pages -[] +[] type SmallGroupInfo = { /// The ID of the small group @@ -243,13 +245,22 @@ type SmallGroupInfo = /// Whether the small group has a publicly-available request list 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 -[] +[] type Church = { /// The ID of this church @@ -286,9 +297,6 @@ type Church = [] 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 -[] +[] 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.) -[] +[] type SmallGroup = { /// The ID of this small group @@ -444,7 +451,7 @@ type SmallGroup = /// This represents a single prayer request -[] +[] type PrayerRequest = { /// The ID of this request @@ -515,7 +522,7 @@ type PrayerRequest = /// This represents a user of PrayerTracker -[] +[] type User = { /// The ID of this user @@ -556,20 +563,3 @@ type User = PasswordHash = "" LastSeen = None SmallGroups = [] } - - -/// Cross-reference between user and small group -[] -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 } diff --git a/src/PrayerTracker.MigrateV9/Program.fs b/src/PrayerTracker.MigrateV9/Program.fs index 0a25360..c2d2bc8 100644 --- a/src/PrayerTracker.MigrateV9/Program.fs +++ b/src/PrayerTracker.MigrateV9/Program.fs @@ -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" diff --git a/src/PrayerTracker/Church.fs b/src/PrayerTracker/Church.fs index 3410c3f..296c536 100644 --- a/src/PrayerTracker/Church.fs +++ b/src/PrayerTracker/Church.fs @@ -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 diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index 32cdd46..a7a7033 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -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"] diff --git a/src/Tests/Data/EntitiesTests.fs b/src/Tests/Data/EntitiesTests.fs index 2efcaa1..12a7bfa 100644 --- a/src/Tests/Data/EntitiesTests.fs +++ b/src/Tests/Data/EntitiesTests.fs @@ -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" } ] - -[] -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" - } - ]