|
|
|
@ -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 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,28 +166,12 @@ 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
|
|
|
|
@ -184,38 +179,24 @@ 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")
|
|
|
|
|
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 ]
|
|
|
|
|
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,7 +233,7 @@ 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)"
|
|
|
|
@ -260,12 +241,11 @@ module PrayerRequests =
|
|
|
|
|
|
|
|
|
|
/// 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")
|
|
|
|
|
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 ]
|
|
|
|
|
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,20 +291,15 @@ 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 =
|
|
|
|
|
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
|
|
|
|
@ -355,14 +307,13 @@ 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")
|
|
|
|
|
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) ]
|
|
|
|
|
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 (
|
|
|
|
@ -524,21 +405,15 @@ module Users =
|
|
|
|
|
|
|
|
|
|
/// 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")
|
|
|
|
|
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"))
|
|
|
|
|
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)) ]
|
|
|
|
|
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 ]
|
|
|
|
|
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 |}
|
|
|
|
|