| 
							
							
							
						 |  |  | @ -1,8 +1,7 @@ | 
		
	
		
			
				|  |  |  |  | namespace PrayerTracker.Data | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | open System | 
		
	
		
			
				|  |  |  |  | open NodaTime | 
		
	
		
			
				|  |  |  |  | open Npgsql | 
		
	
		
			
				|  |  |  |  | open Npgsql.FSharp | 
		
	
		
			
				|  |  |  |  | open PrayerTracker.Entities | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Table names | 
		
	
	
		
			
				
					
					|  |  |  | @ -30,51 +29,75 @@ module Table = | 
		
	
		
			
				|  |  |  |  |     let User = "pt_user" | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// JSON serialization customizations | 
		
	
		
			
				|  |  |  |  | [<RequireQualifiedAccess>] | 
		
	
		
			
				|  |  |  |  | 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) = | 
		
	
		
			
				|  |  |  |  |         inherit JsonConverter<'T>() | 
		
	
		
			
				|  |  |  |  |         override _.Read(reader, _, _) = | 
		
	
		
			
				|  |  |  |  |             wrap (reader.GetString()) | 
		
	
		
			
				|  |  |  |  |         override _.Write(writer, value, _) = | 
		
	
		
			
				|  |  |  |  |             writer.WriteStringValue(unwrap value) | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     open System.Text.Json | 
		
	
		
			
				|  |  |  |  |     open NodaTime.Serialization.SystemTextJson | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// 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) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<PrayerRequestType>(PrayerRequestType.Parse, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<RequestSort>(RequestSort.Parse, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<TimeZoneId>(TimeZoneId, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<ChurchId>(Guid.Parse >> ChurchId, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<MemberId>(Guid.Parse >> MemberId, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<PrayerRequestId>(Guid.Parse >> PrayerRequestId, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<SmallGroupId>(Guid.Parse >> SmallGroupId, string) | 
		
	
		
			
				|  |  |  |  |           WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string) | 
		
	
		
			
				|  |  |  |  |           JsonFSharpConverter() ] | 
		
	
		
			
				|  |  |  |  |         |> List.iter opts.Converters.Add | 
		
	
		
			
				|  |  |  |  |         let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb | 
		
	
		
			
				|  |  |  |  |         opts.PropertyNamingPolicy   <- JsonNamingPolicy.CamelCase | 
		
	
		
			
				|  |  |  |  |         opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull | 
		
	
		
			
				|  |  |  |  |         opts | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | open BitBadger.Documents.Sqlite | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Establish the required data environment | 
		
	
		
			
				|  |  |  |  | [<RequireQualifiedAccess>] | 
		
	
		
			
				|  |  |  |  | module Environment = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Ensure tables and indexes are defined | 
		
	
		
			
				|  |  |  |  |     let setUp () = backgroundTask { | 
		
	
		
			
				|  |  |  |  |         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" ] | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Helper functions for the PostgreSQL data implementation | 
		
	
		
			
				|  |  |  |  | [<AutoOpen>] | 
		
	
		
			
				|  |  |  |  | module private Helpers = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Church instance | 
		
	
		
			
				|  |  |  |  |     let mapToChurch (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id               = ChurchId         (row.uuid "id") | 
		
	
		
			
				|  |  |  |  |             Name             = row.string       "church_name" | 
		
	
		
			
				|  |  |  |  |             City             = row.string       "city" | 
		
	
		
			
				|  |  |  |  |             State            = row.string       "state" | 
		
	
		
			
				|  |  |  |  |             HasVpsInterface  = row.bool         "has_vps_interface" | 
		
	
		
			
				|  |  |  |  |             InterfaceAddress = row.stringOrNone "interface_address" | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a ListPreferences instance | 
		
	
		
			
				|  |  |  |  |     let mapToListPreferences (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   SmallGroupId        = SmallGroupId (row.uuid "small_group_id") | 
		
	
		
			
				|  |  |  |  |             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" | 
		
	
		
			
				|  |  |  |  |             EmailFromAddress    = row.string   "email_from_address" | 
		
	
		
			
				|  |  |  |  |             Fonts               = row.string   "fonts" | 
		
	
		
			
				|  |  |  |  |             HeadingColor        = row.string   "heading_color" | 
		
	
		
			
				|  |  |  |  |             LineColor           = row.string   "line_color" | 
		
	
		
			
				|  |  |  |  |             HeadingFontSize     = row.int      "heading_font_size" | 
		
	
		
			
				|  |  |  |  |             TextFontSize        = row.int      "text_font_size" | 
		
	
		
			
				|  |  |  |  |             GroupPassword       = row.string   "group_password" | 
		
	
		
			
				|  |  |  |  |             IsPublic            = row.bool     "is_public" | 
		
	
		
			
				|  |  |  |  |             PageSize            = row.int      "page_size" | 
		
	
		
			
				|  |  |  |  |             TimeZoneId          = TimeZoneId    (row.string "time_zone_id") | 
		
	
		
			
				|  |  |  |  |             RequestSort         = RequestSort.Parse     (row.string "request_sort") | 
		
	
		
			
				|  |  |  |  |             DefaultEmailType    = EmailFormat.Parse     (row.string "default_email_type") | 
		
	
		
			
				|  |  |  |  |             AsOfDateDisplay     = AsOfDateDisplay.Parse (row.string "as_of_date_display") | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Member instance | 
		
	
		
			
				|  |  |  |  |     let mapToMember (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id           = MemberId         (row.uuid "id") | 
		
	
		
			
				|  |  |  |  |             SmallGroupId = SmallGroupId     (row.uuid "small_group_id") | 
		
	
		
			
				|  |  |  |  |             Name         = row.string       "member_name" | 
		
	
		
			
				|  |  |  |  |             Email        = row.string       "email" | 
		
	
		
			
				|  |  |  |  |             Format       = row.stringOrNone "email_format" |> Option.map EmailFormat.Parse | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Prayer Request instance | 
		
	
		
			
				|  |  |  |  |     let mapToPrayerRequest (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id             = PrayerRequestId         (row.uuid "id") | 
		
	
	
		
			
				
					
					|  |  |  | @ -89,14 +112,6 @@ module private Helpers = | 
		
	
		
			
				|  |  |  |  |             Expiration     = Expiration.Parse        (row.string "expiration") | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Small Group instance | 
		
	
		
			
				|  |  |  |  |     let mapToSmallGroup (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id          = SmallGroupId (row.uuid "id") | 
		
	
		
			
				|  |  |  |  |             ChurchId    = ChurchId     (row.uuid "church_id") | 
		
	
		
			
				|  |  |  |  |             Name        = row.string   "group_name" | 
		
	
		
			
				|  |  |  |  |             Preferences = ListPreferences.Empty | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Small Group information set | 
		
	
		
			
				|  |  |  |  |     let mapToSmallGroupInfo (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id         = Giraffe.ShortGuid.fromGuid (row.uuid "id") | 
		
	
	
		
			
				
					
					|  |  |  | @ -110,12 +125,6 @@ module private Helpers = | 
		
	
		
			
				|  |  |  |  |     let mapToSmallGroupItem (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}""" | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a Small Group instance with populated list preferences | 
		
	
		
			
				|  |  |  |  |     let mapToSmallGroupWithPreferences (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         { mapToSmallGroup row with | 
		
	
		
			
				|  |  |  |  |             Preferences = mapToListPreferences row | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Map a row to a User instance | 
		
	
		
			
				|  |  |  |  |     let mapToUser (row : RowReader) = | 
		
	
		
			
				|  |  |  |  |         {   Id           = UserId     (row.uuid "id") | 
		
	
	
		
			
				
					
					|  |  |  | @ -129,21 +138,23 @@ module private Helpers = | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | open BitBadger.Documents.Postgres | 
		
	
		
			
				|  |  |  |  | open BitBadger.Documents | 
		
	
		
			
				|  |  |  |  | open Npgsql | 
		
	
		
			
				|  |  |  |  | open Npgsql.FSharp | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Functions to manipulate churches | 
		
	
		
			
				|  |  |  |  | module Churches = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a list of all churches | 
		
	
		
			
				|  |  |  |  |     let all () = | 
		
	
		
			
				|  |  |  |  |         Custom.list "SELECT * FROM pt.church ORDER BY church_name" [] mapToChurch | 
		
	
		
			
				|  |  |  |  |         Find.all<Church> Table.Church | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Delete a church by its ID | 
		
	
		
			
				|  |  |  |  |     let deleteById (churchId : ChurchId) = backgroundTask { | 
		
	
		
			
				|  |  |  |  |     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! _ = | 
		
	
		
			
				|  |  |  |  |             Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |             BitBadger.Documents.Postgres.Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |             |> Sql.fromDataSource | 
		
	
		
			
				|  |  |  |  |             |> Sql.executeTransactionAsync | 
		
	
		
			
				|  |  |  |  |                 [   $"DELETE FROM pt.prayer_request {where}", idParam | 
		
	
	
		
			
				
					
					|  |  |  | @ -155,67 +166,37 @@ module Churches = | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Save a church's information | 
		
	
		
			
				|  |  |  |  |     let save (church : Church) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery | 
		
	
		
			
				|  |  |  |  |             "INSERT INTO pt.church ( | 
		
	
		
			
				|  |  |  |  |                 id, church_name, city, state, has_vps_interface, interface_address | 
		
	
		
			
				|  |  |  |  |             ) VALUES ( | 
		
	
		
			
				|  |  |  |  |                 @id, @name, @city, @state, @hasVpsInterface, @interfaceAddress | 
		
	
		
			
				|  |  |  |  |             ) ON CONFLICT (id) DO UPDATE | 
		
	
		
			
				|  |  |  |  |             SET church_name       = EXCLUDED.church_name, | 
		
	
		
			
				|  |  |  |  |                 city              = EXCLUDED.city, | 
		
	
		
			
				|  |  |  |  |                 state             = EXCLUDED.state, | 
		
	
		
			
				|  |  |  |  |                 has_vps_interface = EXCLUDED.has_vps_interface, | 
		
	
		
			
				|  |  |  |  |                 interface_address = EXCLUDED.interface_address" | 
		
	
		
			
				|  |  |  |  |             [   "@id",               Sql.uuid         church.Id.Value | 
		
	
		
			
				|  |  |  |  |                 "@name",             Sql.string       church.Name | 
		
	
		
			
				|  |  |  |  |                 "@city",             Sql.string       church.City | 
		
	
		
			
				|  |  |  |  |                 "@state",            Sql.string       church.State | 
		
	
		
			
				|  |  |  |  |                 "@hasVpsInterface",  Sql.bool         church.HasVpsInterface | 
		
	
		
			
				|  |  |  |  |                 "@interfaceAddress", Sql.stringOrNone church.InterfaceAddress ] | 
		
	
		
			
				|  |  |  |  |     let save church = | 
		
	
		
			
				|  |  |  |  |         save<Church> Table.Church church | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Find a church by its ID | 
		
	
		
			
				|  |  |  |  |     let tryById (churchId : ChurchId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single "SELECT * FROM pt.church WHERE id = @id" [ "@id", Sql.uuid churchId.Value ] mapToChurch | 
		
	
		
			
				|  |  |  |  |     let tryById churchId = | 
		
	
		
			
				|  |  |  |  |         Find.byId<ChurchId, Church> Table.Church churchId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Functions to manipulate small group members | 
		
	
		
			
				|  |  |  |  | module Members = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Count members for the given small group | 
		
	
		
			
				|  |  |  |  |     let countByGroup (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar "SELECT COUNT(id) AS mbr_count FROM pt.member WHERE small_group_id = @groupId" | 
		
	
		
			
				|  |  |  |  |                       [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "mbr_count") | 
		
	
		
			
				|  |  |  |  |     let countByGroup (groupId: SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Delete a small group member by its ID | 
		
	
		
			
				|  |  |  |  |     let deleteById (memberId : MemberId) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery "DELETE FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] | 
		
	
		
			
				|  |  |  |  |     let deleteById (memberId: MemberId) = | 
		
	
		
			
				|  |  |  |  |         Delete.byId Table.Member memberId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Retrieve all members for a given small group | 
		
	
		
			
				|  |  |  |  |     let forGroup (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.list "SELECT * FROM pt.member WHERE small_group_id = @groupId ORDER BY member_name" | 
		
	
		
			
				|  |  |  |  |                     [ "@groupId", Sql.uuid groupId.Value ] mapToMember | 
		
	
		
			
				|  |  |  |  |         Find.byFieldsOrdered<Member> | 
		
	
		
			
				|  |  |  |  |             Table.Member All [ Field.Equal "smallGroupId" groupId ] [ Field.Named "memberName" ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Save a small group member | 
		
	
		
			
				|  |  |  |  |     let save (mbr : Member) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery | 
		
	
		
			
				|  |  |  |  |             "INSERT INTO pt.member ( | 
		
	
		
			
				|  |  |  |  |                 id, small_group_id, member_name, email, email_format | 
		
	
		
			
				|  |  |  |  |             ) VALUES ( | 
		
	
		
			
				|  |  |  |  |                 @id, @groupId, @name, @email, @format | 
		
	
		
			
				|  |  |  |  |             ) ON CONFLICT (id) DO UPDATE | 
		
	
		
			
				|  |  |  |  |             SET member_name  = EXCLUDED.member_name, | 
		
	
		
			
				|  |  |  |  |                 email        = EXCLUDED.email, | 
		
	
		
			
				|  |  |  |  |                 email_format = EXCLUDED.email_format" | 
		
	
		
			
				|  |  |  |  |             [   "@id",      Sql.uuid         mbr.Id.Value | 
		
	
		
			
				|  |  |  |  |                 "@groupId", Sql.uuid         mbr.SmallGroupId.Value | 
		
	
		
			
				|  |  |  |  |                 "@name",    Sql.string       mbr.Name | 
		
	
		
			
				|  |  |  |  |                 "@email",   Sql.string       mbr.Email | 
		
	
		
			
				|  |  |  |  |                 "@format",  Sql.stringOrNone (mbr.Format |> Option.map string) ] | 
		
	
		
			
				|  |  |  |  |     let save mbr = | 
		
	
		
			
				|  |  |  |  |         save<Member> Table.Member mbr | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Retrieve a small group member by its ID | 
		
	
		
			
				|  |  |  |  |     let tryById (memberId : MemberId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single "SELECT * FROM pt.member WHERE id = @id" [ "@id", Sql.uuid memberId.Value ] mapToMember | 
		
	
		
			
				|  |  |  |  |     let tryById memberId = | 
		
	
		
			
				|  |  |  |  |         Find.byId<MemberId, Member> Table.Member memberId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Options to retrieve a list of requests | 
		
	
	
		
			
				
					
					|  |  |  | @ -252,20 +233,19 @@ module PrayerRequests = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Count the number of prayer requests for a church | 
		
	
		
			
				|  |  |  |  |     let countByChurch (churchId : ChurchId) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar | 
		
	
		
			
				|  |  |  |  |         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") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Count the number of prayer requests for a small group | 
		
	
		
			
				|  |  |  |  |     let countByGroup (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar "SELECT COUNT(id) AS req_count FROM pt.prayer_request WHERE small_group_id = @groupId" | 
		
	
		
			
				|  |  |  |  |                       [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "req_count") | 
		
	
		
			
				|  |  |  |  |     let countByGroup (groupId: SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Delete a prayer request by its ID | 
		
	
		
			
				|  |  |  |  |     let deleteById (reqId : PrayerRequestId) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery "DELETE FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ] | 
		
	
		
			
				|  |  |  |  |     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) = | 
		
	
	
		
			
				
					
					|  |  |  | @ -288,7 +268,7 @@ module PrayerRequests = | 
		
	
		
			
				|  |  |  |  |                     "@expecting", Sql.string    (string Expecting) | 
		
	
		
			
				|  |  |  |  |                     "@forced",    Sql.string    (string Forced) ] | 
		
	
		
			
				|  |  |  |  |             else "", [] | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         BitBadger.Documents.Postgres.Custom.list | 
		
	
		
			
				|  |  |  |  |             $"SELECT * | 
		
	
		
			
				|  |  |  |  |                 FROM pt.prayer_request | 
		
	
		
			
				|  |  |  |  |                WHERE small_group_id = @groupId {where} | 
		
	
	
		
			
				
					
					|  |  |  | @ -297,35 +277,12 @@ module PrayerRequests = | 
		
	
		
			
				|  |  |  |  |             (("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Save a prayer request | 
		
	
		
			
				|  |  |  |  |     let save (req : PrayerRequest) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery | 
		
	
		
			
				|  |  |  |  |             "INSERT into pt.prayer_request ( | 
		
	
		
			
				|  |  |  |  |                 id, request_type, user_id, small_group_id, entered_date, updated_date, requestor, request_text, | 
		
	
		
			
				|  |  |  |  |                 notify_chaplain, expiration | 
		
	
		
			
				|  |  |  |  |             ) VALUES ( | 
		
	
		
			
				|  |  |  |  |                 @id, @type, @userId, @groupId, @entered, @updated, @requestor, @text, | 
		
	
		
			
				|  |  |  |  |                 @notifyChaplain, @expiration | 
		
	
		
			
				|  |  |  |  |             ) ON CONFLICT (id) DO UPDATE | 
		
	
		
			
				|  |  |  |  |             SET request_type    = EXCLUDED.request_type, | 
		
	
		
			
				|  |  |  |  |                 updated_date    = EXCLUDED.updated_date, | 
		
	
		
			
				|  |  |  |  |                 requestor       = EXCLUDED.requestor, | 
		
	
		
			
				|  |  |  |  |                 request_text    = EXCLUDED.request_text, | 
		
	
		
			
				|  |  |  |  |                 notify_chaplain = EXCLUDED.notify_chaplain, | 
		
	
		
			
				|  |  |  |  |                 expiration      = EXCLUDED.expiration" | 
		
	
		
			
				|  |  |  |  |             [   "@id",             Sql.uuid         req.Id.Value | 
		
	
		
			
				|  |  |  |  |                 "@type",           Sql.string       (string req.RequestType) | 
		
	
		
			
				|  |  |  |  |                 "@userId",         Sql.uuid         req.UserId.Value | 
		
	
		
			
				|  |  |  |  |                 "@groupId",        Sql.uuid         req.SmallGroupId.Value | 
		
	
		
			
				|  |  |  |  |                 "@entered",        Sql.parameter    (NpgsqlParameter("@entered", req.EnteredDate)) | 
		
	
		
			
				|  |  |  |  |                 "@updated",        Sql.parameter    (NpgsqlParameter("@updated", req.UpdatedDate)) | 
		
	
		
			
				|  |  |  |  |                 "@requestor",      Sql.stringOrNone req.Requestor | 
		
	
		
			
				|  |  |  |  |                 "@text",           Sql.string       req.Text | 
		
	
		
			
				|  |  |  |  |                 "@notifyChaplain", Sql.bool         req.NotifyChaplain | 
		
	
		
			
				|  |  |  |  |                 "@expiration",     Sql.string       (string req.Expiration) ] | 
		
	
		
			
				|  |  |  |  |     let save req = | 
		
	
		
			
				|  |  |  |  |         save<PrayerRequest> Table.Request req | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Search prayer requests for the given term | 
		
	
		
			
				|  |  |  |  |     let searchForGroup group searchTerm pageNbr = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         BitBadger.Documents.Postgres.Custom.list | 
		
	
		
			
				|  |  |  |  |             $"SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND request_text ILIKE @search | 
		
	
		
			
				|  |  |  |  |                   UNION | 
		
	
		
			
				|  |  |  |  |               SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search | 
		
	
	
		
			
				
					
					|  |  |  | @ -334,35 +291,29 @@ module PrayerRequests = | 
		
	
		
			
				|  |  |  |  |             [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Retrieve a prayer request by its ID | 
		
	
		
			
				|  |  |  |  |     let tryById (reqId : PrayerRequestId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single "SELECT * FROM pt.prayer_request WHERE id = @id" [ "@id", Sql.uuid reqId.Value ] | 
		
	
		
			
				|  |  |  |  |                       mapToPrayerRequest | 
		
	
		
			
				|  |  |  |  |     let tryById reqId = | 
		
	
		
			
				|  |  |  |  |         Find.byId<PrayerRequestId, PrayerRequest> Table.Request reqId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Update the expiration for the given prayer request | 
		
	
		
			
				|  |  |  |  |     let updateExpiration (req : PrayerRequest) withTime = | 
		
	
		
			
				|  |  |  |  |         let sql, parameters = | 
		
	
		
			
				|  |  |  |  |     let updateExpiration (req: PrayerRequest) withTime = | 
		
	
		
			
				|  |  |  |  |         if withTime then | 
		
	
		
			
				|  |  |  |  |                 ", updated_date = @updated", | 
		
	
		
			
				|  |  |  |  |                 [ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ] | 
		
	
		
			
				|  |  |  |  |             else "", [] | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id" | 
		
	
		
			
				|  |  |  |  |                         ([ "@expiration", Sql.string (string req.Expiration); "@id", Sql.uuid req.Id.Value ] | 
		
	
		
			
				|  |  |  |  |                          |> List.append parameters) | 
		
	
		
			
				|  |  |  |  |             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) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar "SELECT COUNT(id) AS group_count FROM pt.small_group WHERE church_id = @churchId" | 
		
	
		
			
				|  |  |  |  |                       [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "group_count") | 
		
	
		
			
				|  |  |  |  |     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 { | 
		
	
		
			
				|  |  |  |  |         let idParam = [ [ "@groupId", Sql.uuid groupId.Value ] ] | 
		
	
		
			
				|  |  |  |  |         let! _ = | 
		
	
		
			
				|  |  |  |  |             Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |             BitBadger.Documents.Postgres.Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |             |> Sql.fromDataSource | 
		
	
		
			
				|  |  |  |  |             |> Sql.executeTransactionAsync | 
		
	
		
			
				|  |  |  |  |                 [   "DELETE FROM pt.prayer_request   WHERE small_group_id = @groupId", idParam | 
		
	
	
		
			
				
					
					|  |  |  | @ -374,7 +325,7 @@ module SmallGroups = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get information for all small groups | 
		
	
		
			
				|  |  |  |  |     let infoForAll () = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -384,7 +335,7 @@ module SmallGroups = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a list of small group IDs along with a description that includes the church name | 
		
	
		
			
				|  |  |  |  |     let listAll () = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -393,7 +344,7 @@ module SmallGroups = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a list of small group IDs and descriptions for groups with a group password | 
		
	
		
			
				|  |  |  |  |     let listProtected () = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -404,7 +355,7 @@ module SmallGroups = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a list of small group IDs and descriptions for groups that are public or have a group password | 
		
	
		
			
				|  |  |  |  |     let listPublicAndProtected () = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -415,91 +366,21 @@ module SmallGroups = | 
		
	
		
			
				|  |  |  |  |             [] mapToSmallGroupInfo | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Log on for a small group (includes list preferences) | 
		
	
		
			
				|  |  |  |  |     let logOn (groupId : SmallGroupId) password = | 
		
	
		
			
				|  |  |  |  |         Custom.single | 
		
	
		
			
				|  |  |  |  |             "SELECT sg.*, lp.* | 
		
	
		
			
				|  |  |  |  |                FROM pt.small_group sg | 
		
	
		
			
				|  |  |  |  |                     INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id | 
		
	
		
			
				|  |  |  |  |               WHERE sg.id             = @id | 
		
	
		
			
				|  |  |  |  |                 AND lp.group_password = @password" | 
		
	
		
			
				|  |  |  |  |             [ "@id", Sql.uuid groupId.Value; "@password", Sql.string password ] mapToSmallGroupWithPreferences | 
		
	
		
			
				|  |  |  |  |     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 : SmallGroup) isNew = backgroundTask { | 
		
	
		
			
				|  |  |  |  |         let! _ = | 
		
	
		
			
				|  |  |  |  |             Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |             |> Sql.fromDataSource | 
		
	
		
			
				|  |  |  |  |             |> Sql.executeTransactionAsync [ | 
		
	
		
			
				|  |  |  |  |                 "INSERT INTO pt.small_group ( | 
		
	
		
			
				|  |  |  |  |                         id, church_id, group_name | 
		
	
		
			
				|  |  |  |  |                 ) VALUES ( | 
		
	
		
			
				|  |  |  |  |                     @id, @churchId, @name | 
		
	
		
			
				|  |  |  |  |                 ) ON CONFLICT (id) DO UPDATE | 
		
	
		
			
				|  |  |  |  |                 SET church_id  = EXCLUDED.church_id, | 
		
	
		
			
				|  |  |  |  |                     group_name = EXCLUDED.group_name", | 
		
	
		
			
				|  |  |  |  |                 [ [ "@id",       Sql.uuid   group.Id.Value | 
		
	
		
			
				|  |  |  |  |                     "@churchId", Sql.uuid   group.ChurchId.Value | 
		
	
		
			
				|  |  |  |  |                     "@name",     Sql.string group.Name ] ] | 
		
	
		
			
				|  |  |  |  |                 if isNew then | 
		
	
		
			
				|  |  |  |  |                     "INSERT INTO pt.list_preference (small_group_id) VALUES (@id)", | 
		
	
		
			
				|  |  |  |  |                     [ [ "@id", Sql.uuid group.Id.Value ] ] | 
		
	
		
			
				|  |  |  |  |             ] | 
		
	
		
			
				|  |  |  |  |         () | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     let save group = | 
		
	
		
			
				|  |  |  |  |         save<SmallGroup> Table.Group group | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Save a small group's list preferences | 
		
	
		
			
				|  |  |  |  |     let savePreferences (pref : ListPreferences) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery | 
		
	
		
			
				|  |  |  |  |             "UPDATE pt.list_preference | 
		
	
		
			
				|  |  |  |  |                 SET days_to_keep_new       = @daysToKeepNew, | 
		
	
		
			
				|  |  |  |  |                     days_to_expire         = @daysToExpire, | 
		
	
		
			
				|  |  |  |  |                     long_term_update_weeks = @longTermUpdateWeeks, | 
		
	
		
			
				|  |  |  |  |                     email_from_name        = @emailFromName, | 
		
	
		
			
				|  |  |  |  |                     email_from_address     = @emailFromAddress, | 
		
	
		
			
				|  |  |  |  |                     fonts                  = @fonts, | 
		
	
		
			
				|  |  |  |  |                     heading_color          = @headingColor, | 
		
	
		
			
				|  |  |  |  |                     line_color             = @lineColor, | 
		
	
		
			
				|  |  |  |  |                     heading_font_size      = @headingFontSize, | 
		
	
		
			
				|  |  |  |  |                     text_font_size         = @textFontSize, | 
		
	
		
			
				|  |  |  |  |                     request_sort           = @requestSort, | 
		
	
		
			
				|  |  |  |  |                     group_password         = @groupPassword, | 
		
	
		
			
				|  |  |  |  |                     default_email_type     = @defaultEmailType, | 
		
	
		
			
				|  |  |  |  |                     is_public              = @isPublic, | 
		
	
		
			
				|  |  |  |  |                     time_zone_id           = @timeZoneId, | 
		
	
		
			
				|  |  |  |  |                     page_size              = @pageSize, | 
		
	
		
			
				|  |  |  |  |                     as_of_date_display     = @asOfDateDisplay | 
		
	
		
			
				|  |  |  |  |               WHERE small_group_id = @groupId" | 
		
	
		
			
				|  |  |  |  |             [   "@groupId",             Sql.uuid   pref.SmallGroupId.Value | 
		
	
		
			
				|  |  |  |  |                 "@daysToKeepNew",       Sql.int    pref.DaysToKeepNew | 
		
	
		
			
				|  |  |  |  |                 "@daysToExpire",        Sql.int    pref.DaysToExpire | 
		
	
		
			
				|  |  |  |  |                 "@longTermUpdateWeeks", Sql.int    pref.LongTermUpdateWeeks | 
		
	
		
			
				|  |  |  |  |                 "@emailFromName",       Sql.string pref.EmailFromName | 
		
	
		
			
				|  |  |  |  |                 "@emailFromAddress",    Sql.string pref.EmailFromAddress | 
		
	
		
			
				|  |  |  |  |                 "@fonts",               Sql.string pref.Fonts | 
		
	
		
			
				|  |  |  |  |                 "@headingColor",        Sql.string pref.HeadingColor | 
		
	
		
			
				|  |  |  |  |                 "@lineColor",           Sql.string pref.LineColor | 
		
	
		
			
				|  |  |  |  |                 "@headingFontSize",     Sql.int    pref.HeadingFontSize | 
		
	
		
			
				|  |  |  |  |                 "@textFontSize",        Sql.int    pref.TextFontSize | 
		
	
		
			
				|  |  |  |  |                 "@requestSort",         Sql.string (string pref.RequestSort) | 
		
	
		
			
				|  |  |  |  |                 "@groupPassword",       Sql.string pref.GroupPassword | 
		
	
		
			
				|  |  |  |  |                 "@defaultEmailType",    Sql.string (string pref.DefaultEmailType) | 
		
	
		
			
				|  |  |  |  |                 "@isPublic",            Sql.bool   pref.IsPublic | 
		
	
		
			
				|  |  |  |  |                 "@timeZoneId",          Sql.string (string pref.TimeZoneId) | 
		
	
		
			
				|  |  |  |  |                 "@pageSize",            Sql.int    pref.PageSize | 
		
	
		
			
				|  |  |  |  |                 "@asOfDateDisplay",     Sql.string (string pref.AsOfDateDisplay) ] | 
		
	
		
			
				|  |  |  |  |     let savePreferences (pref: ListPreferences) = | 
		
	
		
			
				|  |  |  |  |         Patch.byId Table.Group pref.SmallGroupId {| Preferences = pref |} | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a small group by its ID | 
		
	
		
			
				|  |  |  |  |     let tryById (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single "SELECT * FROM pt.small_group WHERE id = @id" [ "@id", Sql.uuid groupId.Value ] mapToSmallGroup | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get a small group by its ID with its list preferences populated | 
		
	
		
			
				|  |  |  |  |     let tryByIdWithPreferences (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single | 
		
	
		
			
				|  |  |  |  |             "SELECT sg.*, lp.* | 
		
	
		
			
				|  |  |  |  |                FROM pt.small_group sg | 
		
	
		
			
				|  |  |  |  |                     INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id | 
		
	
		
			
				|  |  |  |  |               WHERE sg.id = @id" | 
		
	
		
			
				|  |  |  |  |             [ "@id", Sql.uuid groupId.Value ] mapToSmallGroupWithPreferences | 
		
	
		
			
				|  |  |  |  |     /// Get a small group by its ID (including list preferences) | 
		
	
		
			
				|  |  |  |  |     let tryById groupId = | 
		
	
		
			
				|  |  |  |  |         Find.byId<SmallGroupId, SmallGroup> Table.Group groupId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  | /// Functions to manipulate users | 
		
	
	
		
			
				
					
					|  |  |  | @ -507,11 +388,11 @@ module Users = | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Retrieve all PrayerTracker users | 
		
	
		
			
				|  |  |  |  |     let all () = | 
		
	
		
			
				|  |  |  |  |         Custom.list "SELECT * FROM pt.pt_user ORDER BY last_name, first_name" [] mapToUser | 
		
	
		
			
				|  |  |  |  |         Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Count the number of users for a church | 
		
	
		
			
				|  |  |  |  |     let countByChurch (churchId : ChurchId) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar | 
		
	
		
			
				|  |  |  |  |         BitBadger.Documents.Postgres.Custom.scalar | 
		
	
		
			
				|  |  |  |  |             "SELECT COUNT(u.id) AS user_count | 
		
	
		
			
				|  |  |  |  |                FROM pt.pt_user u | 
		
	
		
			
				|  |  |  |  |               WHERE EXISTS ( | 
		
	
	
		
			
				
					
					|  |  |  | @ -523,22 +404,16 @@ module Users = | 
		
	
		
			
				|  |  |  |  |             [ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count") | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Count the number of users for a small group | 
		
	
		
			
				|  |  |  |  |     let countByGroup (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.scalar "SELECT COUNT(user_id) AS user_count FROM pt.user_small_group WHERE small_group_id = @groupId" | 
		
	
		
			
				|  |  |  |  |                       [ "@groupId", Sql.uuid groupId.Value ] (fun row -> row.int "user_count") | 
		
	
		
			
				|  |  |  |  |     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) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery "DELETE FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Get the IDs of the small groups for which the given user is authorized | 
		
	
		
			
				|  |  |  |  |     let groupIdsByUserId (userId : UserId) = | 
		
	
		
			
				|  |  |  |  |         Custom.list "SELECT small_group_id FROM pt.user_small_group WHERE user_id = @id" | 
		
	
		
			
				|  |  |  |  |                    [ "@id", Sql.uuid userId.Value ] (fun row -> SmallGroupId (row.uuid "small_group_id")) | 
		
	
		
			
				|  |  |  |  |     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) = | 
		
	
		
			
				|  |  |  |  |         Custom.list | 
		
	
		
			
				|  |  |  |  |         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 | 
		
	
	
		
			
				
					
					|  |  |  | @ -547,68 +422,26 @@ module Users = | 
		
	
		
			
				|  |  |  |  |             [ "@groupId", Sql.uuid groupId.Value ] mapToUser | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Save a user's information | 
		
	
		
			
				|  |  |  |  |     let save (user : User) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery | 
		
	
		
			
				|  |  |  |  |             "INSERT INTO pt.pt_user ( | 
		
	
		
			
				|  |  |  |  |                 id, first_name, last_name, email, is_admin, password_hash | 
		
	
		
			
				|  |  |  |  |             ) VALUES ( | 
		
	
		
			
				|  |  |  |  |                 @id, @firstName, @lastName, @email, @isAdmin, @passwordHash | 
		
	
		
			
				|  |  |  |  |             ) ON CONFLICT (id) DO UPDATE | 
		
	
		
			
				|  |  |  |  |             SET first_name    = EXCLUDED.first_name, | 
		
	
		
			
				|  |  |  |  |                 last_name     = EXCLUDED.last_name, | 
		
	
		
			
				|  |  |  |  |                 email         = EXCLUDED.email, | 
		
	
		
			
				|  |  |  |  |                 is_admin      = EXCLUDED.is_admin, | 
		
	
		
			
				|  |  |  |  |                 password_hash = EXCLUDED.password_hash" | 
		
	
		
			
				|  |  |  |  |             [   "@id",           Sql.uuid   user.Id.Value | 
		
	
		
			
				|  |  |  |  |                 "@firstName",    Sql.string user.FirstName | 
		
	
		
			
				|  |  |  |  |                 "@lastName",     Sql.string user.LastName | 
		
	
		
			
				|  |  |  |  |                 "@email",        Sql.string user.Email | 
		
	
		
			
				|  |  |  |  |                 "@isAdmin",      Sql.bool   user.IsAdmin | 
		
	
		
			
				|  |  |  |  |                 "@passwordHash", Sql.string user.PasswordHash ] | 
		
	
		
			
				|  |  |  |  |     let save user = | 
		
	
		
			
				|  |  |  |  |         save<User> Table.User user | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Find a user by its e-mail address and authorized small group | 
		
	
		
			
				|  |  |  |  |     let tryByEmailAndGroup email (groupId : SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single | 
		
	
		
			
				|  |  |  |  |             "SELECT u.* | 
		
	
		
			
				|  |  |  |  |                FROM pt.pt_user u | 
		
	
		
			
				|  |  |  |  |                     INNER JOIN pt.user_small_group usg ON usg.user_id = u.id AND usg.small_group_id = @groupId | 
		
	
		
			
				|  |  |  |  |               WHERE u.email = @email" | 
		
	
		
			
				|  |  |  |  |             [ "@email", Sql.string email; "@groupId", Sql.uuid groupId.Value ] mapToUser | 
		
	
		
			
				|  |  |  |  |     let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) = | 
		
	
		
			
				|  |  |  |  |         Find.firstByFields<User> | 
		
	
		
			
				|  |  |  |  |             Table.User All [ Field.Equal "email" email; Field.InArray "smallGroups" Table.User [ groupId ] ] | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Find a user by their database ID | 
		
	
		
			
				|  |  |  |  |     let tryById (userId : UserId) = | 
		
	
		
			
				|  |  |  |  |         Custom.single "SELECT * FROM pt.pt_user WHERE id = @id" [ "@id", Sql.uuid userId.Value ] mapToUser | 
		
	
		
			
				|  |  |  |  |     let tryById userId = | 
		
	
		
			
				|  |  |  |  |         Find.byId<UserId, User> Table.User userId | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Update a user's last seen date/time | 
		
	
		
			
				|  |  |  |  |     let updateLastSeen (userId : UserId) (now : Instant) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery "UPDATE pt.pt_user SET last_seen = @now WHERE id = @id" | 
		
	
		
			
				|  |  |  |  |                         [ "@id", Sql.uuid userId.Value; "@now", Sql.parameter (NpgsqlParameter ("@now", now)) ] | 
		
	
		
			
				|  |  |  |  |     let updateLastSeen (userId: UserId) (now: Instant) = | 
		
	
		
			
				|  |  |  |  |         Patch.byId Table.User userId {| LastSeen = now |} | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Update a user's password hash | 
		
	
		
			
				|  |  |  |  |     let updatePassword (user : User) = | 
		
	
		
			
				|  |  |  |  |         Custom.nonQuery "UPDATE pt.pt_user SET password_hash = @passwordHash WHERE id = @id" | 
		
	
		
			
				|  |  |  |  |                         [ "@id", Sql.uuid user.Id.Value; "@passwordHash", Sql.string user.PasswordHash ] | 
		
	
		
			
				|  |  |  |  |     let updatePassword (user: User) = | 
		
	
		
			
				|  |  |  |  |         Patch.byId Table.User user.Id {| PasswordHash = user.PasswordHash |} | 
		
	
		
			
				|  |  |  |  | 
 | 
		
	
		
			
				|  |  |  |  |     /// Update a user's authorized small groups | 
		
	
		
			
				|  |  |  |  |     let updateSmallGroups (userId : UserId) groupIds = backgroundTask { | 
		
	
		
			
				|  |  |  |  |         let! existingGroupIds = groupIdsByUserId userId | 
		
	
		
			
				|  |  |  |  |         let toAdd = | 
		
	
		
			
				|  |  |  |  |             groupIds |> List.filter (fun it -> existingGroupIds |> List.exists (fun grpId -> grpId = it) |> not) | 
		
	
		
			
				|  |  |  |  |         let toDelete = | 
		
	
		
			
				|  |  |  |  |             existingGroupIds |> List.filter (fun it -> groupIds |> List.exists (fun grpId -> grpId = it) |> not) | 
		
	
		
			
				|  |  |  |  |         let queries = seq { | 
		
	
		
			
				|  |  |  |  |             if not (List.isEmpty toAdd) then | 
		
	
		
			
				|  |  |  |  |                 "INSERT INTO pt.user_small_group VALUES (@userId, @smallGroupId)", | 
		
	
		
			
				|  |  |  |  |                 toAdd |> List.map (fun it -> [ "@userId", Sql.uuid userId.Value; "@smallGroupId", Sql.uuid it.Value ]) | 
		
	
		
			
				|  |  |  |  |             if not (List.isEmpty toDelete) then | 
		
	
		
			
				|  |  |  |  |                 "DELETE FROM pt.user_small_group WHERE user_id = @userId AND small_group_id = @smallGroupId", | 
		
	
		
			
				|  |  |  |  |                 toDelete | 
		
	
		
			
				|  |  |  |  |                 |> List.map (fun it -> [ "@userId", Sql.uuid userId.Value; "@smallGroupId", Sql.uuid it.Value ]) | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |         if not (Seq.isEmpty queries) then | 
		
	
		
			
				|  |  |  |  |             let! _ = | 
		
	
		
			
				|  |  |  |  |                 Configuration.dataSource () | 
		
	
		
			
				|  |  |  |  |                 |> Sql.fromDataSource | 
		
	
		
			
				|  |  |  |  |                 |> Sql.executeTransactionAsync (List.ofSeq queries) | 
		
	
		
			
				|  |  |  |  |             () | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |     let updateSmallGroups (userId: UserId) (groupIds: SmallGroupId list) = | 
		
	
		
			
				|  |  |  |  |         Patch.byId Table.User userId {| SmallGroups = groupIds |} | 
		
	
	
		
			
				
					
					| 
							
							
							
						 |  |  | 
 |