|
|
|
@ -1,9 +1,5 @@
|
|
|
|
|
namespace PrayerTracker.Data
|
|
|
|
|
|
|
|
|
|
open System
|
|
|
|
|
open NodaTime
|
|
|
|
|
open PrayerTracker.Entities
|
|
|
|
|
|
|
|
|
|
/// Table names
|
|
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
|
module Table =
|
|
|
|
@ -29,6 +25,10 @@ module Table =
|
|
|
|
|
let User = "pt_user"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open System
|
|
|
|
|
open NodaTime
|
|
|
|
|
open PrayerTracker.Entities
|
|
|
|
|
|
|
|
|
|
/// JSON serialization customizations
|
|
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
|
module Json =
|
|
|
|
@ -36,12 +36,10 @@ module Json =
|
|
|
|
|
open System.Text.Json.Serialization
|
|
|
|
|
|
|
|
|
|
/// Convert a wrapped DU to/from its string representation
|
|
|
|
|
type WrappedJsonConverter<'T>(wrap : string -> 'T, unwrap : 'T -> string) =
|
|
|
|
|
type WrappedJsonConverter<'T>(wrap: string -> 'T, unwrap: 'T -> string) =
|
|
|
|
|
inherit JsonConverter<'T>()
|
|
|
|
|
override _.Read(reader, _, _) =
|
|
|
|
|
wrap (reader.GetString())
|
|
|
|
|
override _.Write(writer, value, _) =
|
|
|
|
|
writer.WriteStringValue(unwrap value)
|
|
|
|
|
override _.Read(reader, _, _) = wrap (reader.GetString())
|
|
|
|
|
override _.Write(writer, value, _) = writer.WriteStringValue(unwrap value)
|
|
|
|
|
|
|
|
|
|
open System.Text.Json
|
|
|
|
|
open NodaTime.Serialization.SystemTextJson
|
|
|
|
@ -49,6 +47,7 @@ module Json =
|
|
|
|
|
/// JSON serializer options to support the target domain
|
|
|
|
|
let options =
|
|
|
|
|
let opts = JsonSerializerOptions()
|
|
|
|
|
|
|
|
|
|
[ WrappedJsonConverter<AsOfDateDisplay>(AsOfDateDisplay.Parse, string) :> JsonConverter
|
|
|
|
|
WrappedJsonConverter<EmailFormat>(EmailFormat.Parse, string)
|
|
|
|
|
WrappedJsonConverter<Expiration>(Expiration.Parse, string)
|
|
|
|
@ -62,121 +61,204 @@ module Json =
|
|
|
|
|
WrappedJsonConverter<UserId>(Guid.Parse >> UserId, string)
|
|
|
|
|
JsonFSharpConverter() ]
|
|
|
|
|
|> List.iter opts.Converters.Add
|
|
|
|
|
|
|
|
|
|
let _ = opts.ConfigureForNodaTime DateTimeZoneProviders.Tzdb
|
|
|
|
|
opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
|
|
|
|
|
opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
|
|
|
|
|
opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull
|
|
|
|
|
opts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open BitBadger.Documents
|
|
|
|
|
open BitBadger.Documents.Sqlite
|
|
|
|
|
|
|
|
|
|
/// Establish the required data environment
|
|
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
|
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<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0)
|
|
|
|
|
if not (List.contains Table.Church tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Church
|
|
|
|
|
if not (List.contains Table.Group tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Group
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ]
|
|
|
|
|
if not (List.contains Table.Member tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Member
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ]
|
|
|
|
|
if not (List.contains Table.Request tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Request
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ]
|
|
|
|
|
if not (List.contains Table.User tables) then
|
|
|
|
|
do! Definition.ensureTable Table.User
|
|
|
|
|
do! Definition.ensureFieldIndex Table.User "email" [ "email" ]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open 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<string> "SELECT table_name FROM sqlite_master" [] _.GetString(0)
|
|
|
|
|
|
|
|
|
|
if not (List.contains Table.Church tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Church
|
|
|
|
|
|
|
|
|
|
if not (List.contains Table.Group tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Group
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Group "church" [ "churchId" ]
|
|
|
|
|
|
|
|
|
|
if not (List.contains Table.Member tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Member
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Member "group" [ "smallGroupId" ]
|
|
|
|
|
|
|
|
|
|
if not (List.contains Table.Request tables) then
|
|
|
|
|
do! Definition.ensureTable Table.Request
|
|
|
|
|
do! Definition.ensureFieldIndex Table.Request "group" [ "smallGroupId" ]
|
|
|
|
|
|
|
|
|
|
if not (List.contains Table.User tables) then
|
|
|
|
|
do! Definition.ensureTable Table.User
|
|
|
|
|
do! Definition.ensureFieldIndex Table.User "email" [ "email" ]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
open Microsoft.Data.Sqlite
|
|
|
|
|
|
|
|
|
|
/// Helper functions for the PostgreSQL data implementation
|
|
|
|
|
[<AutoOpen>]
|
|
|
|
|
module private Helpers =
|
|
|
|
|
|
|
|
|
|
/// Map a row to a Prayer Request instance
|
|
|
|
|
let mapToPrayerRequest (row : RowReader) =
|
|
|
|
|
{ Id = PrayerRequestId (row.uuid "id")
|
|
|
|
|
UserId = UserId (row.uuid "user_id")
|
|
|
|
|
SmallGroupId = SmallGroupId (row.uuid "small_group_id")
|
|
|
|
|
EnteredDate = row.fieldValue<Instant> "entered_date"
|
|
|
|
|
UpdatedDate = row.fieldValue<Instant> "updated_date"
|
|
|
|
|
Requestor = row.stringOrNone "requestor"
|
|
|
|
|
Text = row.string "request_text"
|
|
|
|
|
NotifyChaplain = row.bool "notify_chaplain"
|
|
|
|
|
RequestType = PrayerRequestType.Parse (row.string "request_type")
|
|
|
|
|
Expiration = Expiration.Parse (row.string "expiration")
|
|
|
|
|
}
|
|
|
|
|
let mapToPrayerRequest (row: RowReader) =
|
|
|
|
|
{ Id = PrayerRequestId(row.uuid "id")
|
|
|
|
|
UserId = UserId(row.uuid "user_id")
|
|
|
|
|
SmallGroupId = SmallGroupId(row.uuid "small_group_id")
|
|
|
|
|
EnteredDate = row.fieldValue<Instant> "entered_date"
|
|
|
|
|
UpdatedDate = row.fieldValue<Instant> "updated_date"
|
|
|
|
|
Requestor = row.stringOrNone "requestor"
|
|
|
|
|
Text = row.string "request_text"
|
|
|
|
|
NotifyChaplain = row.bool "notify_chaplain"
|
|
|
|
|
RequestType = PrayerRequestType.Parse(row.string "request_type")
|
|
|
|
|
Expiration = Expiration.Parse(row.string "expiration") }
|
|
|
|
|
|
|
|
|
|
/// Map a row to a Small Group information set
|
|
|
|
|
let mapToSmallGroupInfo (row : RowReader) =
|
|
|
|
|
{ Id = Giraffe.ShortGuid.fromGuid (row.uuid "id")
|
|
|
|
|
Name = row.string "group_name"
|
|
|
|
|
ChurchName = row.string "church_name"
|
|
|
|
|
TimeZoneId = TimeZoneId (row.string "time_zone_id")
|
|
|
|
|
IsPublic = row.bool "is_public"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open Npgsql
|
|
|
|
|
|
|
|
|
|
/// Functions to retrieve small group information
|
|
|
|
|
module SmallGroups =
|
|
|
|
|
|
|
|
|
|
/// Query to retrieve data for a small group info instance
|
|
|
|
|
let private infoQuery =
|
|
|
|
|
$"SELECT g.data->>'id' AS id, g.data->>'groupName' AS groupName, c.data->>'churchName' AS churchName,
|
|
|
|
|
g.data->'preferences'->>'timeZoneId' AS timeZoneId, g.data->'preferences'->>'isPublic' AS isPublic
|
|
|
|
|
FROM {Table.Group} g
|
|
|
|
|
INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'"
|
|
|
|
|
|
|
|
|
|
/// Query to retrieve data for a small group select list item
|
|
|
|
|
let private itemQuery =
|
|
|
|
|
$"SELECT g.data->>'groupName' AS groupName, g.data->>'id' AS id, c.data->>'churchName' AS churchName
|
|
|
|
|
FROM {Table.Group} g
|
|
|
|
|
INNER JOIN {Table.Church} c ON c.data->>'id' = g.data->>'churchId'"
|
|
|
|
|
|
|
|
|
|
/// The ORDER BY clause for select list item queries
|
|
|
|
|
let private itemOrderBy = "ORDER BY c.data->>'churchName', g.data->>'groupName'"
|
|
|
|
|
|
|
|
|
|
/// Map a row to a Small Group list item
|
|
|
|
|
let mapToSmallGroupItem (row : RowReader) =
|
|
|
|
|
Giraffe.ShortGuid.fromGuid (row.uuid "id"), $"""{row.string "church_name"} | {row.string "group_name"}"""
|
|
|
|
|
let private toSmallGroupItem (rdr: SqliteDataReader) =
|
|
|
|
|
(rdr.GetOrdinal >> rdr.GetString >> Guid.Parse >> Giraffe.ShortGuid.fromGuid) "id",
|
|
|
|
|
$"""{(rdr.GetOrdinal >> rdr.GetString) "churchName"} | {(rdr.GetOrdinal >> rdr.GetString) "groupName"}"""
|
|
|
|
|
|
|
|
|
|
/// Map a row to a User instance
|
|
|
|
|
let mapToUser (row : RowReader) =
|
|
|
|
|
{ Id = UserId (row.uuid "id")
|
|
|
|
|
FirstName = row.string "first_name"
|
|
|
|
|
LastName = row.string "last_name"
|
|
|
|
|
Email = row.string "email"
|
|
|
|
|
IsAdmin = row.bool "is_admin"
|
|
|
|
|
PasswordHash = row.string "password_hash"
|
|
|
|
|
LastSeen = row.fieldValueOrNone<Instant> "last_seen"
|
|
|
|
|
SmallGroups = []
|
|
|
|
|
/// Get the group IDs for the given church
|
|
|
|
|
let internal groupIdsByChurch (churchId: ChurchId) =
|
|
|
|
|
backgroundTask {
|
|
|
|
|
let! groups = Find.byFields<SmallGroup> Table.Group All [ Field.Equal "churchId" churchId ]
|
|
|
|
|
return groups |> List.map _.Id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Count the number of small groups for a church
|
|
|
|
|
let countByChurch (churchId: ChurchId) =
|
|
|
|
|
Count.byFields Table.Group All [ Field.Equal "churchId" churchId ]
|
|
|
|
|
|
|
|
|
|
/// Delete a small group by its ID
|
|
|
|
|
let deleteById (groupId: SmallGroupId) =
|
|
|
|
|
backgroundTask {
|
|
|
|
|
use conn = Configuration.dbConn ()
|
|
|
|
|
use! txn = conn.BeginTransactionAsync()
|
|
|
|
|
|
|
|
|
|
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
|
|
|
|
|
for user in users do
|
|
|
|
|
do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |}
|
|
|
|
|
|
|
|
|
|
do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ]
|
|
|
|
|
do! conn.deleteById Table.Group groupId
|
|
|
|
|
|
|
|
|
|
do! txn.CommitAsync()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get information for all small groups
|
|
|
|
|
let infoForAll () =
|
|
|
|
|
Custom.list $"{infoQuery} ORDER BY g.data->>'groupName'" [] SmallGroupInfo.FromReader
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs along with a description that includes the church name
|
|
|
|
|
let listAll () =
|
|
|
|
|
Custom.list $"{itemQuery} {itemOrderBy}" [] toSmallGroupItem
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs and descriptions for groups with a group password
|
|
|
|
|
let listProtected () =
|
|
|
|
|
Custom.list
|
|
|
|
|
$"{itemQuery} WHERE COALESCE(g.data->'preferences'->>'groupPassword', '') <> '' {itemOrderBy}"
|
|
|
|
|
[]
|
|
|
|
|
toSmallGroupItem
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs and descriptions for groups that are public or have a group password
|
|
|
|
|
let listPublicAndProtected () =
|
|
|
|
|
Custom.list
|
|
|
|
|
$"{infoQuery}
|
|
|
|
|
WHERE g.data->'preferences'->>'isPublic' = TRUE
|
|
|
|
|
OR COALESCE(g.data->'preferences'->>'groupPassword', '') <> ''
|
|
|
|
|
ORDER BY c.data->>'churchName', g.data->>'groupName'"
|
|
|
|
|
[]
|
|
|
|
|
SmallGroupInfo.FromReader
|
|
|
|
|
|
|
|
|
|
/// Log on for a small group (includes list preferences)
|
|
|
|
|
let logOn (groupId: SmallGroupId) (password: string) =
|
|
|
|
|
Find.firstByFields<SmallGroup>
|
|
|
|
|
Table.Group
|
|
|
|
|
All
|
|
|
|
|
[ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ]
|
|
|
|
|
|
|
|
|
|
/// Save a small group
|
|
|
|
|
let save group = save<SmallGroup> Table.Group group
|
|
|
|
|
|
|
|
|
|
/// Save a small group's list preferences
|
|
|
|
|
let savePreferences (groupId: SmallGroupId) (pref: ListPreferences) =
|
|
|
|
|
Patch.byId Table.Group groupId {| Preferences = pref |}
|
|
|
|
|
|
|
|
|
|
/// Get a small group by its ID (including list preferences)
|
|
|
|
|
let tryById groupId =
|
|
|
|
|
Find.byId<SmallGroupId, SmallGroup> Table.Group groupId
|
|
|
|
|
|
|
|
|
|
open BitBadger.Documents
|
|
|
|
|
open Npgsql
|
|
|
|
|
open Npgsql.FSharp
|
|
|
|
|
|
|
|
|
|
/// Functions to manipulate churches
|
|
|
|
|
module Churches =
|
|
|
|
|
|
|
|
|
|
/// Get a list of all churches
|
|
|
|
|
let all () =
|
|
|
|
|
Find.all<Church> Table.Church
|
|
|
|
|
let all () = Find.all<Church> Table.Church
|
|
|
|
|
|
|
|
|
|
/// Delete a church by its ID
|
|
|
|
|
let deleteById (churchId: ChurchId) = backgroundTask {
|
|
|
|
|
let idParam = [ [ "@churchId", Sql.uuid churchId.Value ] ]
|
|
|
|
|
let where = "WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
|
|
|
|
|
let! _ =
|
|
|
|
|
BitBadger.Documents.Postgres.Configuration.dataSource ()
|
|
|
|
|
|> Sql.fromDataSource
|
|
|
|
|
|> Sql.executeTransactionAsync
|
|
|
|
|
[ $"DELETE FROM pt.prayer_request {where}", idParam
|
|
|
|
|
$"DELETE FROM pt.user_small_group {where}", idParam
|
|
|
|
|
$"DELETE FROM pt.list_preference {where}", idParam
|
|
|
|
|
"DELETE FROM pt.small_group WHERE church_id = @churchId", idParam
|
|
|
|
|
"DELETE FROM pt.church WHERE id = @churchId", idParam ]
|
|
|
|
|
()
|
|
|
|
|
}
|
|
|
|
|
let deleteById churchId =
|
|
|
|
|
backgroundTask {
|
|
|
|
|
use conn = Configuration.dbConn ()
|
|
|
|
|
use! txn = conn.BeginTransactionAsync()
|
|
|
|
|
|
|
|
|
|
let! groupIds = SmallGroups.groupIdsByChurch churchId
|
|
|
|
|
|
|
|
|
|
do! Delete.byFields Table.Request All [ Field.In "smallGroupId" groupIds ]
|
|
|
|
|
|
|
|
|
|
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User groupIds ]
|
|
|
|
|
|
|
|
|
|
for user in users do
|
|
|
|
|
do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except groupIds |}
|
|
|
|
|
|
|
|
|
|
do! Delete.byFields Table.Group All [ Field.Equal "churchId" churchId ]
|
|
|
|
|
do! Delete.byId Table.Church churchId
|
|
|
|
|
do! txn.CommitAsync()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save a church's information
|
|
|
|
|
let save church =
|
|
|
|
|
save<Church> Table.Church church
|
|
|
|
|
let save church = save<Church> Table.Church church
|
|
|
|
|
|
|
|
|
|
/// Find a church by its ID
|
|
|
|
|
let tryById churchId =
|
|
|
|
@ -191,17 +273,18 @@ module Members =
|
|
|
|
|
Count.byFields Table.Member All [ Field.Equal "smallGroupId" groupId ]
|
|
|
|
|
|
|
|
|
|
/// Delete a small group member by its ID
|
|
|
|
|
let deleteById (memberId: MemberId) =
|
|
|
|
|
Delete.byId Table.Member memberId
|
|
|
|
|
let deleteById (memberId: MemberId) = Delete.byId Table.Member memberId
|
|
|
|
|
|
|
|
|
|
/// Retrieve all members for a given small group
|
|
|
|
|
let forGroup (groupId : SmallGroupId) =
|
|
|
|
|
let forGroup (groupId: SmallGroupId) =
|
|
|
|
|
Find.byFieldsOrdered<Member>
|
|
|
|
|
Table.Member All [ Field.Equal "smallGroupId" groupId ] [ Field.Named "memberName" ]
|
|
|
|
|
Table.Member
|
|
|
|
|
All
|
|
|
|
|
[ Field.Equal "smallGroupId" groupId ]
|
|
|
|
|
[ Field.Named "memberName" ]
|
|
|
|
|
|
|
|
|
|
/// Save a small group member
|
|
|
|
|
let save mbr =
|
|
|
|
|
save<Member> Table.Member mbr
|
|
|
|
|
let save mbr = save<Member> Table.Member mbr
|
|
|
|
|
|
|
|
|
|
/// Retrieve a small group member by its ID
|
|
|
|
|
let tryById memberId =
|
|
|
|
@ -210,20 +293,21 @@ module Members =
|
|
|
|
|
|
|
|
|
|
/// Options to retrieve a list of requests
|
|
|
|
|
type PrayerRequestOptions =
|
|
|
|
|
{ /// The small group for which requests should be retrieved
|
|
|
|
|
SmallGroup : SmallGroup
|
|
|
|
|
{
|
|
|
|
|
/// The small group for which requests should be retrieved
|
|
|
|
|
SmallGroup: SmallGroup
|
|
|
|
|
|
|
|
|
|
/// The clock instance to use for date/time manipulation
|
|
|
|
|
Clock : IClock
|
|
|
|
|
Clock: IClock
|
|
|
|
|
|
|
|
|
|
/// The date for which the list is being retrieved
|
|
|
|
|
ListDate : LocalDate option
|
|
|
|
|
ListDate: LocalDate option
|
|
|
|
|
|
|
|
|
|
/// Whether only active requests should be retrieved
|
|
|
|
|
ActiveOnly : bool
|
|
|
|
|
ActiveOnly: bool
|
|
|
|
|
|
|
|
|
|
/// The page number, for paged lists
|
|
|
|
|
PageNumber : int
|
|
|
|
|
PageNumber: int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -237,57 +321,66 @@ module PrayerRequests =
|
|
|
|
|
| SortByRequestor -> "requestor, updated_date DESC, entered_date DESC"
|
|
|
|
|
|
|
|
|
|
/// Paginate a prayer request query
|
|
|
|
|
let private paginate (pageNbr : int) pageSize =
|
|
|
|
|
if pageNbr > 0 then $"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}" else ""
|
|
|
|
|
let private paginate (pageNbr: int) pageSize =
|
|
|
|
|
if pageNbr > 0 then
|
|
|
|
|
$"LIMIT {pageSize} OFFSET {(pageNbr - 1) * pageSize}"
|
|
|
|
|
else
|
|
|
|
|
""
|
|
|
|
|
|
|
|
|
|
/// Count the number of prayer requests for a church
|
|
|
|
|
let countByChurch (churchId : ChurchId) =
|
|
|
|
|
let countByChurch (churchId: ChurchId) =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.scalar
|
|
|
|
|
"SELECT COUNT(id) AS req_count
|
|
|
|
|
FROM pt.prayer_request
|
|
|
|
|
WHERE small_group_id IN (SELECT id FROM pt.small_group WHERE church_id = @churchId)"
|
|
|
|
|
[ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "req_count")
|
|
|
|
|
[ "@churchId", Sql.uuid churchId.Value ]
|
|
|
|
|
(fun row -> row.int "req_count")
|
|
|
|
|
|
|
|
|
|
/// Count the number of prayer requests for a small group
|
|
|
|
|
let countByGroup (groupId: SmallGroupId) =
|
|
|
|
|
Count.byFields Table.Request All [ Field.Equal "smallGroupId" groupId ]
|
|
|
|
|
|
|
|
|
|
/// Delete a prayer request by its ID
|
|
|
|
|
let deleteById (reqId: PrayerRequestId) =
|
|
|
|
|
Delete.byId Table.Request reqId
|
|
|
|
|
let deleteById (reqId: PrayerRequestId) = Delete.byId Table.Request reqId
|
|
|
|
|
|
|
|
|
|
/// Get all (or active) requests for a small group as of now or the specified date
|
|
|
|
|
let forGroup (opts : PrayerRequestOptions) =
|
|
|
|
|
let forGroup (opts: PrayerRequestOptions) =
|
|
|
|
|
let theDate = defaultArg opts.ListDate (opts.SmallGroup.LocalDateNow opts.Clock)
|
|
|
|
|
|
|
|
|
|
let where, parameters =
|
|
|
|
|
if opts.ActiveOnly then
|
|
|
|
|
let asOf = NpgsqlParameter (
|
|
|
|
|
"@asOf",
|
|
|
|
|
(theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone)
|
|
|
|
|
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
|
|
|
|
|
.ToInstant ())
|
|
|
|
|
let asOf =
|
|
|
|
|
NpgsqlParameter(
|
|
|
|
|
"@asOf",
|
|
|
|
|
(theDate.AtStartOfDayInZone(opts.SmallGroup.TimeZone)
|
|
|
|
|
- Duration.FromDays opts.SmallGroup.Preferences.DaysToExpire)
|
|
|
|
|
.ToInstant()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
" AND ( updated_date > @asOf
|
|
|
|
|
OR expiration = @manual
|
|
|
|
|
OR request_type = @longTerm
|
|
|
|
|
OR request_type = @expecting)
|
|
|
|
|
AND expiration <> @forced",
|
|
|
|
|
[ "@asOf", Sql.parameter asOf
|
|
|
|
|
"@manual", Sql.string (string Manual)
|
|
|
|
|
"@longTerm", Sql.string (string LongTermRequest)
|
|
|
|
|
"@expecting", Sql.string (string Expecting)
|
|
|
|
|
"@forced", Sql.string (string Forced) ]
|
|
|
|
|
else "", []
|
|
|
|
|
[ "@asOf", Sql.parameter asOf
|
|
|
|
|
"@manual", Sql.string (string Manual)
|
|
|
|
|
"@longTerm", Sql.string (string LongTermRequest)
|
|
|
|
|
"@expecting", Sql.string (string Expecting)
|
|
|
|
|
"@forced", Sql.string (string Forced) ]
|
|
|
|
|
else
|
|
|
|
|
"", []
|
|
|
|
|
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
$"SELECT *
|
|
|
|
|
FROM pt.prayer_request
|
|
|
|
|
WHERE small_group_id = @groupId {where}
|
|
|
|
|
ORDER BY {orderBy opts.SmallGroup.Preferences.RequestSort}
|
|
|
|
|
{paginate opts.PageNumber opts.SmallGroup.Preferences.PageSize}"
|
|
|
|
|
(("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters) mapToPrayerRequest
|
|
|
|
|
(("@groupId", Sql.uuid opts.SmallGroup.Id.Value) :: parameters)
|
|
|
|
|
mapToPrayerRequest
|
|
|
|
|
|
|
|
|
|
/// Save a prayer request
|
|
|
|
|
let save req =
|
|
|
|
|
save<PrayerRequest> Table.Request req
|
|
|
|
|
let save req = save<PrayerRequest> Table.Request req
|
|
|
|
|
|
|
|
|
|
/// Search prayer requests for the given term
|
|
|
|
|
let searchForGroup group searchTerm pageNbr =
|
|
|
|
@ -297,7 +390,9 @@ module PrayerRequests =
|
|
|
|
|
SELECT * FROM pt.prayer_request WHERE small_group_id = @groupId AND COALESCE(requestor, '') ILIKE @search
|
|
|
|
|
ORDER BY {orderBy group.Preferences.RequestSort}
|
|
|
|
|
{paginate pageNbr group.Preferences.PageSize}"
|
|
|
|
|
[ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] mapToPrayerRequest
|
|
|
|
|
[ "@groupId", Sql.uuid group.Id.Value
|
|
|
|
|
"@search", Sql.string $"%%%s{searchTerm}%%" ]
|
|
|
|
|
mapToPrayerRequest
|
|
|
|
|
|
|
|
|
|
/// Retrieve a prayer request by its ID
|
|
|
|
|
let tryById reqId =
|
|
|
|
@ -306,92 +401,15 @@ module PrayerRequests =
|
|
|
|
|
/// Update the expiration for the given prayer request
|
|
|
|
|
let updateExpiration (req: PrayerRequest) withTime =
|
|
|
|
|
if withTime then
|
|
|
|
|
Patch.byId Table.Request req.Id {| UpdatedDate = req.UpdatedDate; Expiration = req.Expiration |}
|
|
|
|
|
Patch.byId
|
|
|
|
|
Table.Request
|
|
|
|
|
req.Id
|
|
|
|
|
{| UpdatedDate = req.UpdatedDate
|
|
|
|
|
Expiration = req.Expiration |}
|
|
|
|
|
else
|
|
|
|
|
Patch.byId Table.Request req.Id {| Expiration = req.Expiration |}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Functions to retrieve small group information
|
|
|
|
|
module SmallGroups =
|
|
|
|
|
|
|
|
|
|
/// Count the number of small groups for a church
|
|
|
|
|
let countByChurch (churchId: ChurchId) =
|
|
|
|
|
Count.byFields Table.Group All [ Field.Equal "churchId" churchId ]
|
|
|
|
|
|
|
|
|
|
/// Delete a small group by its ID
|
|
|
|
|
let deleteById (groupId: SmallGroupId) = backgroundTask {
|
|
|
|
|
use conn = Configuration.dbConn ()
|
|
|
|
|
use txn = conn.BeginTransaction()
|
|
|
|
|
|
|
|
|
|
let! users = Find.byFields<User> Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
for user in users do
|
|
|
|
|
do! Patch.byId Table.User user.Id {| SmallGroups = user.SmallGroups |> List.except [ groupId ] |}
|
|
|
|
|
do! conn.deleteByFields Table.Request All [ Field.Equal "smallGroupId" groupId ]
|
|
|
|
|
do! conn.deleteById Table.Group groupId
|
|
|
|
|
|
|
|
|
|
do! txn.CommitAsync()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get information for all small groups
|
|
|
|
|
let infoForAll () =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
"SELECT sg.id, sg.group_name, c.church_name, lp.time_zone_id, lp.is_public
|
|
|
|
|
FROM pt.small_group sg
|
|
|
|
|
INNER JOIN pt.church c ON c.id = sg.church_id
|
|
|
|
|
INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id
|
|
|
|
|
ORDER BY sg.group_name"
|
|
|
|
|
[] mapToSmallGroupInfo
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs along with a description that includes the church name
|
|
|
|
|
let listAll () =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
"SELECT g.group_name, g.id, c.church_name
|
|
|
|
|
FROM pt.small_group g
|
|
|
|
|
INNER JOIN pt.church c ON c.id = g.church_id
|
|
|
|
|
ORDER BY c.church_name, g.group_name"
|
|
|
|
|
[] mapToSmallGroupItem
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs and descriptions for groups with a group password
|
|
|
|
|
let listProtected () =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
"SELECT g.group_name, g.id, c.church_name, lp.is_public
|
|
|
|
|
FROM pt.small_group g
|
|
|
|
|
INNER JOIN pt.church c ON c.id = g.church_id
|
|
|
|
|
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
|
|
|
|
WHERE COALESCE(lp.group_password, '') <> ''
|
|
|
|
|
ORDER BY c.church_name, g.group_name"
|
|
|
|
|
[] mapToSmallGroupItem
|
|
|
|
|
|
|
|
|
|
/// Get a list of small group IDs and descriptions for groups that are public or have a group password
|
|
|
|
|
let listPublicAndProtected () =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
"SELECT g.group_name, g.id, c.church_name, lp.time_zone_id, lp.is_public
|
|
|
|
|
FROM pt.small_group g
|
|
|
|
|
INNER JOIN pt.church c ON c.id = g.church_id
|
|
|
|
|
INNER JOIN pt.list_preference lp ON lp.small_group_id = g.id
|
|
|
|
|
WHERE lp.is_public = TRUE
|
|
|
|
|
OR COALESCE(lp.group_password, '') <> ''
|
|
|
|
|
ORDER BY c.church_name, g.group_name"
|
|
|
|
|
[] mapToSmallGroupInfo
|
|
|
|
|
|
|
|
|
|
/// Log on for a small group (includes list preferences)
|
|
|
|
|
let logOn (groupId: SmallGroupId) (password: string) =
|
|
|
|
|
Find.firstByFields<SmallGroup>
|
|
|
|
|
Table.Group All [ Field.Equal "id" groupId; Field.Equal "preferences.groupPassword" password ]
|
|
|
|
|
|
|
|
|
|
/// Save a small group
|
|
|
|
|
let save group =
|
|
|
|
|
save<SmallGroup> Table.Group group
|
|
|
|
|
|
|
|
|
|
/// Save a small group's list preferences
|
|
|
|
|
let savePreferences (pref: ListPreferences) =
|
|
|
|
|
Patch.byId Table.Group pref.SmallGroupId {| Preferences = pref |}
|
|
|
|
|
|
|
|
|
|
/// Get a small group by its ID (including list preferences)
|
|
|
|
|
let tryById groupId =
|
|
|
|
|
Find.byId<SmallGroupId, SmallGroup> Table.Group groupId
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Functions to manipulate users
|
|
|
|
|
module Users =
|
|
|
|
|
|
|
|
|
@ -400,44 +418,37 @@ module Users =
|
|
|
|
|
Find.allOrdered<User> Table.User [ Field.Named "lastName"; Field.Named "firstName" ]
|
|
|
|
|
|
|
|
|
|
/// Count the number of users for a church
|
|
|
|
|
let countByChurch (churchId : ChurchId) =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.scalar
|
|
|
|
|
"SELECT COUNT(u.id) AS user_count
|
|
|
|
|
FROM pt.pt_user u
|
|
|
|
|
WHERE EXISTS (
|
|
|
|
|
SELECT 1
|
|
|
|
|
FROM pt.user_small_group usg
|
|
|
|
|
INNER JOIN pt.small_group sg ON sg.id = usg.small_group_id
|
|
|
|
|
WHERE usg.user_id = u.id
|
|
|
|
|
AND sg.church_id = @churchId)"
|
|
|
|
|
[ "@churchId", Sql.uuid churchId.Value ] (fun row -> row.int "user_count")
|
|
|
|
|
let countByChurch churchId =
|
|
|
|
|
backgroundTask {
|
|
|
|
|
let! groupIds = SmallGroups.groupIdsByChurch churchId
|
|
|
|
|
return! Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User groupIds ]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Count the number of users for a small group
|
|
|
|
|
let countByGroup (groupId: SmallGroupId) =
|
|
|
|
|
Count.byFields Table.User All [ Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
|
|
|
|
|
/// Delete a user by its database ID
|
|
|
|
|
let deleteById (userId: UserId) =
|
|
|
|
|
Delete.byId Table.User userId
|
|
|
|
|
let deleteById (userId: UserId) = Delete.byId Table.User userId
|
|
|
|
|
|
|
|
|
|
/// Get a list of users authorized to administer the given small group
|
|
|
|
|
let listByGroupId (groupId : SmallGroupId) =
|
|
|
|
|
BitBadger.Documents.Postgres.Custom.list
|
|
|
|
|
"SELECT u.*
|
|
|
|
|
FROM pt.pt_user u
|
|
|
|
|
INNER JOIN pt.user_small_group usg ON usg.user_id = u.id
|
|
|
|
|
WHERE usg.small_group_id = @groupId
|
|
|
|
|
ORDER BY u.last_name, u.first_name"
|
|
|
|
|
[ "@groupId", Sql.uuid groupId.Value ] mapToUser
|
|
|
|
|
let listByGroupId (groupId: SmallGroupId) =
|
|
|
|
|
Find.byFieldsOrdered<User>
|
|
|
|
|
Table.User
|
|
|
|
|
All
|
|
|
|
|
[ Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
[ Field.Named "lastName"; Field.Named "firstName" ]
|
|
|
|
|
|
|
|
|
|
/// Save a user's information
|
|
|
|
|
let save user =
|
|
|
|
|
save<User> Table.User user
|
|
|
|
|
let save user = save<User> Table.User user
|
|
|
|
|
|
|
|
|
|
/// Find a user by its e-mail address and authorized small group
|
|
|
|
|
let tryByEmailAndGroup (email: string) (groupId: SmallGroupId) =
|
|
|
|
|
Find.firstByFields<User>
|
|
|
|
|
Table.User All [ Field.Equal "email" email; Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
Table.User
|
|
|
|
|
All
|
|
|
|
|
[ Field.Equal "email" email
|
|
|
|
|
Field.InArray "smallGroups" Table.User [ groupId ] ]
|
|
|
|
|
|
|
|
|
|
/// Find a user by their database ID
|
|
|
|
|
let tryById userId =
|
|
|
|
|