diff --git a/src/PrayerTracker.Data/Access.fs b/src/PrayerTracker.Data/Access.fs index e554df1..e717ba7 100644 --- a/src/PrayerTracker.Data/Access.fs +++ b/src/PrayerTracker.Data/Access.fs @@ -39,7 +39,6 @@ module private Helpers = RequestSort = RequestSort.fromCode (row.string "request_sort") DefaultEmailType = EmailFormat.fromCode (row.string "default_email_type") AsOfDateDisplay = AsOfDateDisplay.fromCode (row.string "as_of_date_display") - TimeZone = TimeZone.empty } /// Map a row to a Member instance @@ -49,7 +48,6 @@ module private Helpers = Name = row.string "member_name" Email = row.string "email" Format = row.stringOrNone "email_format" |> Option.map EmailFormat.fromCode - SmallGroup = SmallGroup.empty } /// Map a row to a Prayer Request instance @@ -64,8 +62,6 @@ module private Helpers = NotifyChaplain = row.bool "notify_chaplain" RequestType = PrayerRequestType.fromCode (row.string "request_id") Expiration = Expiration.fromCode (row.string "expiration") - User = User.empty - SmallGroup = SmallGroup.empty } /// Map a row to a Small Group instance @@ -74,10 +70,6 @@ module private Helpers = ChurchId = ChurchId (row.uuid "church_id") Name = row.string "group_name" Preferences = ListPreferences.empty - Church = Church.empty - Members = ResizeArray () - PrayerRequests = ResizeArray () - Users = ResizeArray () } /// Map a row to a Small Group information set @@ -107,9 +99,7 @@ module private Helpers = Email = row.string "email" IsAdmin = row.bool "is_admin" PasswordHash = row.string "password_hash" - Salt = None LastSeen = row.fieldValueOrNone "last_seen" - SmallGroups = ResizeArray () } @@ -373,6 +363,19 @@ module PrayerRequests = return () } + /// Search prayer requests for the given term + let searchForGroup group searchTerm pageNbr conn = + conn + |> Sql.existingConnection + |> Sql.query $""" + 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 + ORDER BY {orderBy group.Preferences.RequestSort} + {paginate pageNbr group.Preferences.PageSize}""" + |> Sql.parameters [ "@groupId", Sql.uuid group.Id.Value; "@search", Sql.string $"%%%s{searchTerm}%%" ] + |> Sql.executeAsync mapToPrayerRequest + /// Retrieve a prayer request by its ID let tryById (reqId : PrayerRequestId) conn = backgroundTask { let! req = @@ -385,14 +388,20 @@ module PrayerRequests = } /// Update the expiration for the given prayer request - let updateExpiration (req : PrayerRequest) conn = backgroundTask { + let updateExpiration (req : PrayerRequest) withTime conn = backgroundTask { + let sql, parameters = + if withTime then + ", updated_date = @updated", + [ "@updated", Sql.parameter (NpgsqlParameter ("@updated", req.UpdatedDate)) ] + else "", [] let! _ = conn |> Sql.existingConnection - |> Sql.query "UPDATE pt.prayer_request SET expiration = @expiration WHERE id = @id" + |> Sql.query $"UPDATE pt.prayer_request SET expiration = @expiration{sql} WHERE id = @id" |> Sql.parameters - [ "@expiration", Sql.string (Expiration.toCode req.Expiration) + ([ "@expiration", Sql.string (Expiration.toCode req.Expiration) "@id", Sql.uuid req.Id.Value ] + |> List.append parameters) |> Sql.executeNonQueryAsync return () } diff --git a/src/PrayerTracker.Data/AppDbContext.fs b/src/PrayerTracker.Data/AppDbContext.fs deleted file mode 100644 index da8cb3a..0000000 --- a/src/PrayerTracker.Data/AppDbContext.fs +++ /dev/null @@ -1,86 +0,0 @@ -namespace PrayerTracker - -open Microsoft.EntityFrameworkCore -open PrayerTracker.Entities - -/// EF Core data context for PrayerTracker -[] -type AppDbContext (options : DbContextOptions) = - inherit DbContext (options) - - [] - val mutable private churches : DbSet - [] - val mutable private members : DbSet - [] - val mutable private prayerRequests : DbSet - [] - val mutable private preferences : DbSet - [] - val mutable private smallGroups : DbSet - [] - val mutable private timeZones : DbSet - [] - val mutable private users : DbSet - [] - val mutable private userGroupXref : DbSet - - /// Churches - member this.Churches - with get() = this.churches - and set v = this.churches <- v - - /// Small group members - member this.Members - with get() = this.members - and set v = this.members <- v - - /// Prayer requests - member this.PrayerRequests - with get() = this.prayerRequests - and set v = this.prayerRequests <- v - - /// Request list preferences (by class) - member this.Preferences - with get() = this.preferences - and set v = this.preferences <- v - - /// Small groups - member this.SmallGroups - with get() = this.smallGroups - and set v = this.smallGroups <- v - - /// Time zones - member this.TimeZones - with get() = this.timeZones - and set v = this.timeZones <- v - - /// Users - member this.Users - with get() = this.users - and set v = this.users <- v - - /// User / small group cross-reference - member this.UserGroupXref - with get() = this.userGroupXref - and set v = this.userGroupXref <- v - - override _.OnConfiguring (optionsBuilder : DbContextOptionsBuilder) = - base.OnConfiguring optionsBuilder - optionsBuilder.UseQueryTrackingBehavior QueryTrackingBehavior.NoTracking |> ignore - - override _.OnModelCreating (modelBuilder : ModelBuilder) = - base.OnModelCreating modelBuilder - - modelBuilder.HasDefaultSchema "pt" |> ignore - - [ Church.ConfigureEF - ListPreferences.ConfigureEF - Member.ConfigureEF - PrayerRequest.ConfigureEF - SmallGroup.ConfigureEF - TimeZone.ConfigureEF - User.ConfigureEF - UserSmallGroup.ConfigureEF - ] - |> List.iter (fun x -> x modelBuilder) diff --git a/src/PrayerTracker.Data/DataAccess.fs b/src/PrayerTracker.Data/DataAccess.fs deleted file mode 100644 index ef7169e..0000000 --- a/src/PrayerTracker.Data/DataAccess.fs +++ /dev/null @@ -1,257 +0,0 @@ -[] -module PrayerTracker.DataAccess - -open System.Linq -open NodaTime -open PrayerTracker.Entities - -[] -module private Helpers = - - /// Central place to append sort criteria for prayer request queries - let reqSort sort (q : IQueryable) = - match sort with - | SortByDate -> - q.OrderByDescending(fun req -> req.UpdatedDate) - .ThenByDescending(fun req -> req.EnteredDate) - .ThenBy (fun req -> req.Requestor) - | SortByRequestor -> - q.OrderBy(fun req -> req.Requestor) - .ThenByDescending(fun req -> req.UpdatedDate) - .ThenByDescending (fun req -> req.EnteredDate) - - /// Paginate a prayer request query - let paginate (pageNbr : int) pageSize (q : IQueryable) = - if pageNbr > 0 then q.Skip((pageNbr - 1) * pageSize).Take pageSize else q - - -open Microsoft.EntityFrameworkCore -open Microsoft.FSharpLu - -type AppDbContext with - - (*-- DISCONNECTED DATA EXTENSIONS --*) - - /// Add an entity entry to the tracked data context with the status of Added - member this.AddEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) = - this.Entry<'TEntity>(e).State <- EntityState.Added - - /// Add an entity entry to the tracked data context with the status of Updated - member this.UpdateEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) = - this.Entry<'TEntity>(e).State <- EntityState.Modified - - /// Add an entity entry to the tracked data context with the status of Deleted - member this.RemoveEntry<'TEntity when 'TEntity : not struct> (e : 'TEntity) = - this.Entry<'TEntity>(e).State <- EntityState.Deleted - - (*-- CHURCH EXTENSIONS --*) - - /// Find a church by its Id - member this.TryChurchById churchId = backgroundTask { - let! church = this.Churches.SingleOrDefaultAsync (fun ch -> ch.Id = churchId) - return Option.fromObject church - } - - /// Find all churches - member this.AllChurches () = backgroundTask { - let! churches = this.Churches.OrderBy(fun ch -> ch.Name).ToListAsync () - return List.ofSeq churches - } - - (*-- MEMBER EXTENSIONS --*) - - /// Get a small group member by its Id - member this.TryMemberById memberId = backgroundTask { - let! mbr = this.Members.SingleOrDefaultAsync (fun m -> m.Id = memberId) - return Option.fromObject mbr - } - - /// Find all members for a small group - member this.AllMembersForSmallGroup groupId = backgroundTask { - let! members = - this.Members.Where(fun mbr -> mbr.SmallGroupId = groupId) - .OrderBy(fun mbr -> mbr.Name) - .ToListAsync () - return List.ofSeq members - } - - /// Count members for a small group - member this.CountMembersForSmallGroup groupId = backgroundTask { - return! this.Members.CountAsync (fun m -> m.SmallGroupId = groupId) - } - - (*-- PRAYER REQUEST EXTENSIONS --*) - - /// Get a prayer request by its Id - member this.TryRequestById reqId = backgroundTask { - let! req = this.PrayerRequests.SingleOrDefaultAsync (fun r -> r.Id = reqId) - return Option.fromObject req - } - - /// Get all (or active) requests for a small group as of now or the specified date - member this.AllRequestsForSmallGroup (grp : SmallGroup) clock listDate activeOnly pageNbr = backgroundTask { - let theDate = match listDate with Some dt -> dt | _ -> SmallGroup.localDateNow clock grp - let query = - this.PrayerRequests.Where(fun req -> req.SmallGroupId = grp.Id) - |> function - | q when activeOnly -> - let asOf = - (theDate.AtStartOfDayInZone(SmallGroup.timeZone grp) - Duration.FromDays grp.Preferences.DaysToExpire) - .ToInstant () - q.Where(fun req -> - ( req.UpdatedDate > asOf - || req.Expiration = Manual - || req.RequestType = LongTermRequest - || req.RequestType = Expecting) - && req.Expiration <> Forced) - |> reqSort grp.Preferences.RequestSort - |> paginate pageNbr grp.Preferences.PageSize - | q -> reqSort grp.Preferences.RequestSort q - let! reqs = query.ToListAsync () - return List.ofSeq reqs - } - - /// Count prayer requests for the given small group Id - member this.CountRequestsBySmallGroup groupId = backgroundTask { - return! this.PrayerRequests.CountAsync (fun pr -> pr.SmallGroupId = groupId) - } - - /// Count prayer requests for the given church Id - member this.CountRequestsByChurch churchId = backgroundTask { - return! this.PrayerRequests.CountAsync (fun pr -> pr.SmallGroup.ChurchId = churchId) - } - - /// Search requests for a small group using the given case-insensitive search term - member this.SearchRequestsForSmallGroup (grp : SmallGroup) (searchTerm : string) pageNbr = backgroundTask { - let sql = """ - SELECT * FROM pt.prayer_request WHERE small_group_id = {0} AND request_text ILIKE {1} - UNION - SELECT * FROM pt.prayer_request WHERE small_group_id = {0} AND COALESCE(requestor, '') ILIKE {1}""" - let like = sprintf "%%%s%%" - let query = - this.PrayerRequests.FromSqlRaw (sql, grp.Id.Value, like searchTerm) - |> reqSort grp.Preferences.RequestSort - |> paginate pageNbr grp.Preferences.PageSize - let! reqs = query.ToListAsync () - return List.ofSeq reqs - } - - (*-- SMALL GROUP EXTENSIONS --*) - - /// Find a small group by its Id - member this.TryGroupById groupId = backgroundTask { - let! grp = - this.SmallGroups.Include(fun sg -> sg.Preferences) - .SingleOrDefaultAsync (fun sg -> sg.Id = groupId) - return Option.fromObject grp - } - - /// Get small groups that are public or password protected - member this.PublicAndProtectedGroups () = backgroundTask { - let! groups = - this.SmallGroups.Include(fun sg -> sg.Preferences).Include(fun sg -> sg.Church) - .Where(fun sg -> - sg.Preferences.IsPublic - || (sg.Preferences.GroupPassword <> null && sg.Preferences.GroupPassword <> "")) - .OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name) - .ToListAsync () - return List.ofSeq groups - } - - /// Get small groups that are password protected - member this.ProtectedGroups () = backgroundTask { - let! groups = - this.SmallGroups.Include(fun sg -> sg.Church) - .Where(fun sg -> sg.Preferences.GroupPassword <> null && sg.Preferences.GroupPassword <> "") - .OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name) - .ToListAsync () - return List.ofSeq groups - } - - /// Get all small groups - member this.AllGroups () = backgroundTask { - let! groups = - this.SmallGroups - .Include(fun sg -> sg.Church) - .Include(fun sg -> sg.Preferences) - .Include(fun sg -> sg.Preferences.TimeZone) - .OrderBy(fun sg -> sg.Name) - .ToListAsync () - return List.ofSeq groups - } - - /// Get a small group list by their Id, with their church prepended to their name - member this.GroupList () = backgroundTask { - let! groups = - this.SmallGroups.Include(fun sg -> sg.Church) - .OrderBy(fun sg -> sg.Church.Name).ThenBy(fun sg -> sg.Name) - .ToListAsync () - return - groups - |> Seq.map (fun sg -> Giraffe.ShortGuid.fromGuid sg.Id.Value, $"{sg.Church.Name} | {sg.Name}") - |> List.ofSeq - } - - /// Log on a small group - member this.TryGroupLogOnByPassword groupId pw = backgroundTask { - match! this.TryGroupById groupId with - | Some grp when pw = grp.Preferences.GroupPassword -> return Some grp - | _ -> return None - } - - /// Count small groups for the given church Id - member this.CountGroupsByChurch churchId = backgroundTask { - return! this.SmallGroups.CountAsync (fun sg -> sg.ChurchId = churchId) - } - - (*-- TIME ZONE EXTENSIONS --*) - - /// Get all time zones - member this.AllTimeZones () = backgroundTask { - let! zones = this.TimeZones.OrderBy(fun tz -> tz.SortOrder).ToListAsync () - return List.ofSeq zones - } - - (*-- USER EXTENSIONS --*) - - /// Find a user by its Id - member this.TryUserById userId = backgroundTask { - let! usr = this.Users.SingleOrDefaultAsync (fun u -> u.Id = userId) - return Option.fromObject usr - } - - /// Find a user by its e-mail address and authorized small group - member this.TryUserByEmailAndGroup email groupId = backgroundTask { - let! usr = - this.Users.SingleOrDefaultAsync (fun u -> - u.Email = email && u.SmallGroups.Any (fun xref -> xref.SmallGroupId = groupId)) - return Option.fromObject usr - } - - /// Find a user by its Id, eagerly loading the user's groups - member this.TryUserByIdWithGroups userId = backgroundTask { - let! usr = this.Users.Include(fun u -> u.SmallGroups).SingleOrDefaultAsync (fun u -> u.Id = userId) - return Option.fromObject usr - } - - /// Get a list of all users - member this.AllUsers () = backgroundTask { - let! users = this.Users.OrderBy(fun u -> u.LastName).ThenBy(fun u -> u.FirstName).ToListAsync () - return List.ofSeq users - } - - /// Get all PrayerTracker users as members (used to send e-mails) - member this.AllUsersAsMembers () = backgroundTask { - let! users = this.AllUsers () - return users |> List.map (fun u -> { Member.empty with Email = u.Email; Name = u.Name }) - } - - /// Count the number of users for a small group - member this.CountUsersBySmallGroup groupId = backgroundTask { - return! this.Users.CountAsync (fun u -> u.SmallGroups.Any (fun xref -> xref.SmallGroupId = groupId)) - } - - /// Count the number of users for a church - member this.CountUsersByChurch churchId = backgroundTask { - return! this.Users.CountAsync (fun u -> u.SmallGroups.Any (fun xref -> xref.SmallGroup.ChurchId = churchId)) - } diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index 2a1ecf7..f037225 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -179,184 +179,7 @@ with /// The GUID value of the user ID member this.Value = this |> function UserId guid -> guid - -/// EF Core value converters for the discriminated union types above -module Converters = - - open Microsoft.EntityFrameworkCore.Storage.ValueConversion - open Microsoft.FSharp.Linq.RuntimeHelpers - open System.Linq.Expressions - - let private asOfFromDU = - <@ Func(AsOfDateDisplay.toCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private asOfToDU = - <@ Func(AsOfDateDisplay.fromCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private churchIdFromDU = - <@ Func(fun it -> it.Value) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private churchIdToDU = - <@ Func(ChurchId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private emailFromDU = - <@ Func(EmailFormat.toCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private emailToDU = - <@ Func(EmailFormat.fromCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private emailOptionFromDU = - <@ Func(fun opt -> - match opt with Some fmt -> EmailFormat.toCode fmt | None -> null) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private emailOptionToDU = - <@ Func(fun opt -> - match opt with "" | null -> None | it -> Some (EmailFormat.fromCode it)) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private expFromDU = - <@ Func(Expiration.toCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private expToDU = - <@ Func(Expiration.fromCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private memberIdFromDU = - <@ Func(fun it -> it.Value) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private memberIdToDU = - <@ Func(MemberId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private prayerReqIdFromDU = - <@ Func(fun it -> it.Value) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private prayerReqIdToDU = - <@ Func(PrayerRequestId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private smallGrpIdFromDU = - <@ Func(fun it -> it.Value) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private smallGrpIdToDU = - <@ Func(SmallGroupId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private sortFromDU = - <@ Func(RequestSort.toCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private sortToDU = - <@ Func(RequestSort.fromCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private typFromDU = - <@ Func(PrayerRequestType.toCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private typToDU = - <@ Func(PrayerRequestType.fromCode) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private tzIdFromDU = - <@ Func(TimeZoneId.toString) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private tzIdToDU = - <@ Func(TimeZoneId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private userIdFromDU = - <@ Func(fun it -> it.Value) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - let private userIdToDU = - <@ Func(UserId) @> - |> LeafExpressionConverter.QuotationToExpression - |> unbox>> - - /// Conversion between a string and an AsOfDateDisplay DU value - type AsOfDateDisplayConverter () = - inherit ValueConverter (asOfFromDU, asOfToDU) - - /// Conversion between a GUID and a church ID - type ChurchIdConverter () = - inherit ValueConverter (churchIdFromDU, churchIdToDU) - - /// Conversion between a string and an EmailFormat DU value - type EmailFormatConverter () = - inherit ValueConverter (emailFromDU, emailToDU) - - /// Conversion between a string an an optional EmailFormat DU value - type EmailFormatOptionConverter () = - inherit ValueConverter (emailOptionFromDU, emailOptionToDU) - - /// Conversion between a string and an Expiration DU value - type ExpirationConverter () = - inherit ValueConverter (expFromDU, expToDU) - - /// Conversion between a GUID and a member ID - type MemberIdConverter () = - inherit ValueConverter (memberIdFromDU, memberIdToDU) - - /// Conversion between a GUID and a prayer request ID - type PrayerRequestIdConverter () = - inherit ValueConverter (prayerReqIdFromDU, prayerReqIdToDU) - - /// Conversion between a string and a PrayerRequestType DU value - type PrayerRequestTypeConverter () = - inherit ValueConverter (typFromDU, typToDU) - - /// Conversion between a string and a RequestSort DU value - type RequestSortConverter () = - inherit ValueConverter (sortFromDU, sortToDU) - - /// Conversion between a GUID and a small group ID - type SmallGroupIdConverter () = - inherit ValueConverter (smallGrpIdFromDU, smallGrpIdToDU) - - /// Conversion between a string and a time zone ID - type TimeZoneIdConverter () = - inherit ValueConverter (tzIdFromDU, tzIdToDU) - - /// Conversion between a GUID and a user ID - type UserIdConverter () = - inherit ValueConverter (userIdFromDU, userIdToDU) - +(*-- SPECIFIC VIEW TYPES --*) /// Statistics for churches [] @@ -371,14 +194,33 @@ type ChurchStats = Users : int } + +/// Information needed to display the public/protected request list and small group maintenance pages +[] +type SmallGroupInfo = + { /// The ID of the small group + Id : string + + /// The name of the small group + Name : string + + /// The name of the church to which the small group belongs + ChurchName : string + + /// The ID of the time zone for the small group + TimeZoneId : TimeZoneId + + /// Whether the small group has a publicly-available request list + IsPublic : bool + } + (*-- ENTITIES --*) -open FSharp.EFCore.OptionConverter -open Microsoft.EntityFrameworkCore open NodaTime /// This represents a church -type [] Church = +[] +type Church = { /// The ID of this church Id : ChurchId @@ -397,10 +239,13 @@ type [] Church = /// The address for the interface InterfaceAddress : string option } -with + +/// Functions to support churches +module Church = + /// An empty church // aww... how sad :( - static member empty = + let empty = { Id = ChurchId Guid.Empty Name = "" City = "" @@ -409,27 +254,10 @@ with InterfaceAddress = None } - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "church" - it.Property(fun c -> c.Id).HasColumnName "id" - it.Property(fun c -> c.Name).HasColumnName("church_name").IsRequired () - it.Property(fun c -> c.City).HasColumnName("city").IsRequired () - it.Property(fun c -> c.State).HasColumnName("state").IsRequired().HasMaxLength 2 - it.Property(fun c -> c.HasVpsInterface).HasColumnName "has_vps_interface" - it.Property(fun c -> c.InterfaceAddress).HasColumnName "interface_address" - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof Church.empty.Id) - .SetValueConverter (Converters.ChurchIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof Church.empty.InterfaceAddress) - .SetValueConverter (OptionConverter ()) - /// Preferences for the form and format of the prayer request list -and [] ListPreferences = +[] +type ListPreferences = { /// The Id of the small group to which these preferences belong SmallGroupId : SmallGroupId @@ -478,19 +306,18 @@ and [] ListPreferences = /// The time zone which this class uses (use tzdata names) TimeZoneId : TimeZoneId - /// The time zone information - TimeZone : TimeZone - /// The number of requests displayed per page PageSize : int /// How the as-of date should be automatically displayed AsOfDateDisplay : AsOfDateDisplay } -with + +/// Functions to support list preferences +module ListPreferences = /// A set of preferences with their default values - static member empty = + let empty = { SmallGroupId = SmallGroupId Guid.Empty DaysToExpire = 14 DaysToKeepNew = 7 @@ -507,61 +334,14 @@ with DefaultEmailType = HtmlFormat IsPublic = false TimeZoneId = TimeZoneId "America/Denver" - TimeZone = TimeZone.empty PageSize = 100 AsOfDateDisplay = NoDisplay } - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "list_preference" - it.HasKey (fun lp -> lp.SmallGroupId :> obj) - it.Property(fun lp -> lp.SmallGroupId).HasColumnName "small_group_id" - it.Property(fun lp -> lp.DaysToKeepNew).HasColumnName("days_to_keep_new").IsRequired().HasDefaultValue 7 - it.Property(fun lp -> lp.DaysToExpire).HasColumnName("days_to_expire").IsRequired().HasDefaultValue 14 - it.Property(fun lp -> lp.LongTermUpdateWeeks).HasColumnName("long_term_update_weeks").IsRequired() - .HasDefaultValue 4 - it.Property(fun lp -> lp.EmailFromName).HasColumnName("email_from_name").IsRequired() - .HasDefaultValue "PrayerTracker" - it.Property(fun lp -> lp.EmailFromAddress).HasColumnName("email_from_address").IsRequired() - .HasDefaultValue "prayer@djs-consulting.com" - it.Property(fun lp -> lp.Fonts).HasColumnName("fonts").IsRequired() - .HasDefaultValue "Century Gothic,Tahoma,Luxi Sans,sans-serif" - it.Property(fun lp -> lp.HeadingColor).HasColumnName("heading_color").IsRequired() - .HasDefaultValue "maroon" - it.Property(fun lp -> lp.LineColor).HasColumnName("line_color").IsRequired().HasDefaultValue "navy" - it.Property(fun lp -> lp.HeadingFontSize).HasColumnName("heading_font_size").IsRequired() - .HasDefaultValue 16 - it.Property(fun lp -> lp.TextFontSize).HasColumnName("text_font_size").IsRequired().HasDefaultValue 12 - it.Property(fun lp -> lp.RequestSort).HasColumnName("request_sort").IsRequired().HasMaxLength(1) - .HasDefaultValue SortByDate - it.Property(fun lp -> lp.GroupPassword).HasColumnName("group_password").IsRequired().HasDefaultValue "" - it.Property(fun lp -> lp.DefaultEmailType).HasColumnName("default_email_type").IsRequired() - .HasDefaultValue HtmlFormat - it.Property(fun lp -> lp.IsPublic).HasColumnName("is_public").IsRequired().HasDefaultValue false - it.Property(fun lp -> lp.TimeZoneId).HasColumnName("time_zone_id").IsRequired() - .HasDefaultValue (TimeZoneId "America/Denver") - it.Property(fun lp -> lp.PageSize).HasColumnName("page_size").IsRequired().HasDefaultValue 100 - it.Property(fun lp -> lp.AsOfDateDisplay).HasColumnName("as_of_date_display").IsRequired() - .HasMaxLength(1).HasDefaultValue NoDisplay - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.SmallGroupId) - .SetValueConverter (Converters.SmallGroupIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.RequestSort) - .SetValueConverter (Converters.RequestSortConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.DefaultEmailType) - .SetValueConverter (Converters.EmailFormatConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.TimeZoneId) - .SetValueConverter (Converters.TimeZoneIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof ListPreferences.empty.AsOfDateDisplay) - .SetValueConverter (Converters.AsOfDateDisplayConverter ()) /// A member of a small group -and [] Member = +[] +type Member = { /// The ID of the small group member Id : MemberId @@ -576,44 +356,24 @@ and [] Member = /// The type of e-mail preferred by this member Format : EmailFormat option - - /// The small group to which this member belongs - SmallGroup : SmallGroup } -with + +/// Functions to support small group members +module Member = /// An empty member - static member empty = + let empty = { Id = MemberId Guid.Empty SmallGroupId = SmallGroupId Guid.Empty Name = "" Email = "" Format = None - SmallGroup = SmallGroup.empty } - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "member" - it.Property(fun m -> m.Id).HasColumnName "id" - it.Property(fun m -> m.SmallGroupId).HasColumnName("small_group_id").IsRequired () - it.Property(fun m -> m.Name).HasColumnName("member_name").IsRequired () - it.Property(fun m -> m.Email).HasColumnName("email").IsRequired () - it.Property(fun m -> m.Format).HasColumnName "email_format" - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.Id) - .SetValueConverter (Converters.MemberIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.SmallGroupId) - .SetValueConverter (Converters.SmallGroupIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof Member.empty.Format) - .SetValueConverter (Converters.EmailFormatOptionConverter ()) /// This represents a single prayer request -and [] PrayerRequest = +[] +type PrayerRequest = { /// The ID of this request Id : PrayerRequestId @@ -641,66 +401,15 @@ and [] PrayerRequest = /// Whether the chaplain should be notified for this request NotifyChaplain : bool - /// The user who entered this request - User : User - - /// The small group to which this request belongs - SmallGroup : SmallGroup - /// Is this request expired? Expiration : Expiration } -with - - /// An empty request - static member empty = - { Id = PrayerRequestId Guid.Empty - RequestType = CurrentRequest - UserId = UserId Guid.Empty - SmallGroupId = SmallGroupId Guid.Empty - EnteredDate = Instant.MinValue - UpdatedDate = Instant.MinValue - Requestor = None - Text = "" - NotifyChaplain = false - User = User.empty - SmallGroup = SmallGroup.empty - Expiration = Automatic - } - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "prayer_request" - it.Property(fun pr -> pr.Id).HasColumnName "id" - it.Property(fun pr -> pr.RequestType).HasColumnName("request_type").IsRequired () - it.Property(fun pr -> pr.UserId).HasColumnName "user_id" - it.Property(fun pr -> pr.SmallGroupId).HasColumnName "small_group_id" - it.Property(fun pr -> pr.EnteredDate).HasColumnName "entered_date" - it.Property(fun pr -> pr.UpdatedDate).HasColumnName "updated_date" - it.Property(fun pr -> pr.Requestor).HasColumnName "requestor" - it.Property(fun pr -> pr.Text).HasColumnName("request_text").IsRequired () - it.Property(fun pr -> pr.NotifyChaplain).HasColumnName "notify_chaplain" - it.Property(fun pr -> pr.Expiration).HasColumnName "expiration" - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Id) - .SetValueConverter (Converters.PrayerRequestIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.RequestType) - .SetValueConverter (Converters.PrayerRequestTypeConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.UserId) - .SetValueConverter (Converters.UserIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.SmallGroupId) - .SetValueConverter (Converters.SmallGroupIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Requestor) - .SetValueConverter (OptionConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof PrayerRequest.empty.Expiration) - .SetValueConverter (Converters.ExpirationConverter ()) +// functions are below small group functions /// This represents a small group (Sunday School class, Bible study group, etc.) -and [] SmallGroup = +[] +type SmallGroup = { /// The ID of this small group Id : SmallGroupId @@ -710,215 +419,21 @@ and [] SmallGroup = /// The name of the group Name : string - /// The church to which this small group belongs - Church : Church - /// The preferences for the request list Preferences : ListPreferences - - /// The members of the group - Members : ResizeArray - - /// Prayer requests for this small group - PrayerRequests : ResizeArray - - /// The users authorized to manage this group - Users : ResizeArray } -with - - /// An empty small group - static member empty = - { Id = SmallGroupId Guid.Empty - ChurchId = ChurchId Guid.Empty - Name = "" - Church = Church.empty - Preferences = ListPreferences.empty - Members = ResizeArray () - PrayerRequests = ResizeArray () - Users = ResizeArray () - } - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "small_group" - it.Property(fun sg -> sg.Id).HasColumnName "id" - it.Property(fun sg -> sg.ChurchId).HasColumnName "church_id" - it.Property(fun sg -> sg.Name).HasColumnName("group_name").IsRequired () - it.HasOne(fun sg -> sg.Preferences) - .WithOne() - .HasPrincipalKey(fun sg -> sg.Id :> obj) - .HasForeignKey(fun (lp : ListPreferences) -> lp.SmallGroupId :> obj) - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof SmallGroup.empty.Id) - .SetValueConverter (Converters.SmallGroupIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof SmallGroup.empty.ChurchId) - .SetValueConverter (Converters.ChurchIdConverter ()) - - -/// This represents a time zone in which a class may reside -and [] TimeZone = - { /// The Id for this time zone (uses tzdata names) - Id : TimeZoneId - - /// The description of this time zone - Description : string - - /// The order in which this timezone should be displayed - SortOrder : int - - /// Whether this timezone is active - IsActive : bool - } -with - - /// An empty time zone - static member empty = - { Id = TimeZoneId "" - Description = "" - SortOrder = 0 - IsActive = false - } - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "time_zone" - it.Property(fun tz -> tz.Id).HasColumnName "id" - it.Property(fun tz -> tz.Description).HasColumnName("description").IsRequired () - it.Property(fun tz -> tz.SortOrder).HasColumnName "sort_order" - it.Property(fun tz -> tz.IsActive).HasColumnName "is_active" - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof TimeZone.empty.Id) - .SetValueConverter (Converters.TimeZoneIdConverter ()) - - -/// This represents a user of PrayerTracker -and [] User = - { /// The ID of this user - Id : UserId - - /// The first name of this user - FirstName : string - - /// The last name of this user - LastName : string - - /// The e-mail address of the user - Email : string - - /// Whether this user is a PrayerTracker system administrator - IsAdmin : bool - - /// The user's hashed password - PasswordHash : string - - /// The salt for the user's hashed password - Salt : Guid option - - /// The last time the user was seen (set whenever the user is loaded into a session) - LastSeen : Instant option - - /// The small groups which this user is authorized - SmallGroups : ResizeArray - } -with - - /// An empty user - static member empty = - { Id = UserId Guid.Empty - FirstName = "" - LastName = "" - Email = "" - IsAdmin = false - PasswordHash = "" - Salt = None - LastSeen = None - SmallGroups = ResizeArray () - } - - /// The full name of the user - member this.Name = - $"{this.FirstName} {this.LastName}" - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "pt_user" - it.Ignore(fun u -> u.Name :> obj) - it.Property(fun u -> u.Id).HasColumnName "id" - it.Property(fun u -> u.FirstName).HasColumnName("first_name").IsRequired () - it.Property(fun u -> u.LastName).HasColumnName("last_name").IsRequired () - it.Property(fun u -> u.Email).HasColumnName("email").IsRequired () - it.Property(fun u -> u.IsAdmin).HasColumnName "is_admin" - it.Property(fun u -> u.PasswordHash).HasColumnName("password_hash").IsRequired () - it.Property(fun u -> u.Salt).HasColumnName "salt" - it.Property(fun u -> u.LastSeen).HasColumnName "last_seen" - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.Id) - .SetValueConverter (Converters.UserIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.Salt) - .SetValueConverter (OptionConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof User.empty.LastSeen) - .SetValueConverter (OptionConverter ()) - - -/// Cross-reference between user and small group -and [] UserSmallGroup = - { /// The Id of the user who has access to the small group - UserId : UserId - - /// The Id of the small group to which the user has access - SmallGroupId : SmallGroupId - - /// The user who has access to the small group - User : User - - /// The small group to which the user has access - SmallGroup : SmallGroup - } -with - - /// An empty user/small group xref - static member empty = - { UserId = UserId Guid.Empty - SmallGroupId = SmallGroupId Guid.Empty - User = User.empty - SmallGroup = SmallGroup.empty - } - - /// Configure EF for this entity - static member internal ConfigureEF (mb : ModelBuilder) = - mb.Entity (fun it -> - seq { - it.ToTable "user_small_group" - it.HasKey (nameof UserSmallGroup.empty.UserId, nameof UserSmallGroup.empty.SmallGroupId) - it.Property(fun usg -> usg.UserId).HasColumnName "user_id" - it.Property(fun usg -> usg.SmallGroupId).HasColumnName "small_group_id" - it.HasOne(fun usg -> usg.User) - .WithMany(fun u -> u.SmallGroups :> seq) - .HasForeignKey(fun usg -> usg.UserId :> obj) - it.HasOne(fun usg -> usg.SmallGroup) - .WithMany(fun sg -> sg.Users :> seq) - .HasForeignKey(fun usg -> usg.SmallGroupId :> obj) - } |> List.ofSeq |> ignore) - |> ignore - mb.Model.FindEntityType(typeof).FindProperty(nameof UserSmallGroup.empty.UserId) - .SetValueConverter (Converters.UserIdConverter ()) - mb.Model.FindEntityType(typeof).FindProperty(nameof UserSmallGroup.empty.SmallGroupId) - .SetValueConverter (Converters.SmallGroupIdConverter ()) - - -/// Support functions for small groups +/// Functions to support small groups module SmallGroup = + /// An empty small group + let empty = + { Id = SmallGroupId Guid.Empty + ChurchId = ChurchId Guid.Empty + Name = "" + Preferences = ListPreferences.empty + } + /// The DateTimeZone for the time zone ID for this small group let timeZone group = let tzId = TimeZoneId.toString group.Preferences.TimeZoneId @@ -933,12 +448,25 @@ module SmallGroup = /// Get the local date for this group let localDateNow clock group = (localTimeNow clock group).Date - -/// Support functions for prayer requests +/// Functions to support prayer requests module PrayerRequest = + /// An empty request + let empty = + { Id = PrayerRequestId Guid.Empty + RequestType = CurrentRequest + UserId = UserId Guid.Empty + SmallGroupId = SmallGroupId Guid.Empty + EnteredDate = Instant.MinValue + UpdatedDate = Instant.MinValue + Requestor = None + Text = "" + NotifyChaplain = false + Expiration = Automatic + } + /// Is this request expired? let isExpired (asOf : LocalDate) group req = match req.Expiration, req.RequestType with @@ -958,21 +486,65 @@ module PrayerRequest = >= req.UpdatedDate.InZone(SmallGroup.timeZone group).Date -/// Information needed to display the public/protected request list and small group maintenance pages +/// This represents a user of PrayerTracker [] -type SmallGroupInfo = - { /// The ID of the small group - Id : string +type User = + { /// The ID of this user + Id : UserId - /// The name of the small group - Name : string + /// The first name of this user + FirstName : string - /// The name of the church to which the small group belongs - ChurchName : string + /// The last name of this user + LastName : string - /// The ID of the time zone for the small group - TimeZoneId : TimeZoneId + /// The e-mail address of the user + Email : string - /// Whether the small group has a publicly-available request list - IsPublic : bool + /// Whether this user is a PrayerTracker system administrator + IsAdmin : bool + + /// The user's hashed password + PasswordHash : string + + /// The last time the user was seen (set whenever the user is loaded into a session) + LastSeen : Instant option } +with + /// The full name of the user + member this.Name = + $"{this.FirstName} {this.LastName}" + +/// Functions to support users +module User = + + /// An empty user + let empty = + { Id = UserId Guid.Empty + FirstName = "" + LastName = "" + Email = "" + IsAdmin = false + PasswordHash = "" + LastSeen = None + } + + +/// Cross-reference between user and small group +[] +type UserSmallGroup = + { /// The Id of the user who has access to the small group + UserId : UserId + + /// The Id of the small group to which the user has access + SmallGroupId : SmallGroupId + } + +/// Functions to support user/small group cross-reference +module UserSmallGroup = + + /// An empty user/small group xref + let empty = + { UserId = UserId Guid.Empty + SmallGroupId = SmallGroupId Guid.Empty + } diff --git a/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs b/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs deleted file mode 100644 index ca33df6..0000000 --- a/src/PrayerTracker.Data/Migrations/20161217153124_InitialDatabase.fs +++ /dev/null @@ -1,391 +0,0 @@ -namespace PrayerTracker.Migrations - -open System -open Microsoft.EntityFrameworkCore -open Microsoft.EntityFrameworkCore.Infrastructure -open Microsoft.EntityFrameworkCore.Migrations -open Npgsql.EntityFrameworkCore.PostgreSQL.Metadata -open PrayerTracker -open PrayerTracker.Entities - -[)>] -[] -type InitialDatabase () = - inherit Migration () - override _.Up (migrationBuilder : MigrationBuilder) = - migrationBuilder.EnsureSchema (name = "pt") - |> ignore - - migrationBuilder.CreateTable ( - name = "church", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - City = table.Column (name = "city", nullable = false) - HasVpsInterface = table.Column (name = "has_vps_interface", nullable = false) - InterfaceAddress = table.Column (name = "interface_address", nullable = true) - Name = table.Column (name = "church_Name", nullable = false) - State = table.Column (name = "state", nullable = false, maxLength = Nullable 2) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_church", fun x -> upcast x.Id) |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "time_zone", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - Description = table.Column (name = "description", nullable = false) - IsActive = table.Column (name = "is_active", nullable = false) - SortOrder = table.Column (name = "sort_order", nullable = false) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_time_zone", fun x -> upcast x.Id) |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "pt_user", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - Email = table.Column (name = "email", nullable = false) - FirstName = table.Column (name = "first_name", nullable = false) - IsAdmin = table.Column (name = "is_admin", nullable = false) - LastName = table.Column (name = "last_name", nullable = false) - PasswordHash = table.Column (name = "password_hash", nullable = false) - Salt = table.Column (name = "salt", nullable = true) - LastSeen = table.Column (name = "last_seen", nullable = true) - |}), - constraints = fun table -> - table.PrimaryKey("pk_pt_user", fun x -> upcast x.Id) |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "small_group", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - ChurchId = table.Column (name = "church_id", nullable = false) - Name = table.Column (name = "group_name", nullable = false) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_small_group", fun x -> upcast x.Id) |> ignore - table.ForeignKey ( - name = "fk_small_group_church_id", - column = (fun x -> upcast x.ChurchId), - principalSchema = "pt", - principalTable = "church", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "list_preference", - schema = "pt", - columns = (fun table -> - {| SmallGroupId = table.Column (name = "small_group_id", nullable = false) - AsOfDateDisplay = table.Column (name = "as_of_date_display", nullable = false, defaultValue = "N", maxLength = Nullable 1) - DaysToExpire = table.Column (name = "days_to_expire", nullable = false, defaultValue = 14) - DaysToKeepNew = table.Column (name = "days_to_keep_new", nullable = false, defaultValue = 7) - DefaultEmailType = table.Column (name = "default_email_type", nullable = false, defaultValue = "Html") - EmailFromAddress = table.Column (name = "email_from_address", nullable = false, defaultValue = "prayer@djs-consulting.com") - EmailFromName = table.Column (name = "email_from_name", nullable = false, defaultValue = "PrayerTracker") - Fonts = table.Column (name = "fonts", nullable = false, defaultValue = "Century Gothic,Tahoma,Luxi Sans,sans-serif") - GroupPassword = table.Column (name = "group_password", nullable = false, defaultValue = "") - HeadingColor = table.Column (name = "heading_color", nullable = false, defaultValue = "maroon") - HeadingFontSize = table.Column (name = "heading_font_size", nullable = false, defaultValue = 16) - IsPublic = table.Column (name = "is_public", nullable = false, defaultValue = false) - LineColor = table.Column (name = "line_color", nullable = false, defaultValue = "navy") - LongTermUpdateWeeks = table.Column (name = "long_term_update_weeks", nullable = false, defaultValue = 4) - PageSize = table.Column (name = "page_size", nullable = false, defaultValue = 100) - RequestSort = table.Column (name = "request_sort", nullable = false, defaultValue = "D", maxLength = Nullable 1) - TextFontSize = table.Column (name = "text_font_size", nullable = false, defaultValue = 12) - TimeZoneId = table.Column (name = "time_zone_id", nullable = false, defaultValue = "America/Denver") - |}), - constraints = fun table -> - table.PrimaryKey ("pk_list_preference", fun x -> upcast x.SmallGroupId) |> ignore - table.ForeignKey ( - name = "fk_list_preference_small_group_id", - column = (fun x -> upcast x.SmallGroupId), - principalSchema = "pt", - principalTable = "small_group", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore - table.ForeignKey ( - name = "fk_list_preference_time_zone_id", - column = (fun x -> upcast x.TimeZoneId), - principalSchema = "pt", - principalTable = "time_zone", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "member", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - Email = table.Column (name = "email", nullable = false) - Format = table.Column (name = "email_format", nullable = true) - Name = table.Column (name = "member_name", nullable = false) - SmallGroupId = table.Column (name = "small_group_id", nullable = false) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_member", fun x -> upcast x.Id) |> ignore - table.ForeignKey ( - name = "fk_member_small_group_id", - column = (fun x -> upcast x.SmallGroupId), - principalSchema = "pt", - principalTable = "small_group", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore) - |> ignore - - migrationBuilder.CreateTable ( - name = "prayer_request", - schema = "pt", - columns = (fun table -> - {| Id = table.Column (name = "id", nullable = false) - Expiration = table.Column (name = "expiration", nullable = false) - EnteredDate = table.Column (name = "entered_date", nullable = false) - NotifyChaplain = table.Column (name = "notify_chaplain", nullable = false) - RequestType = table.Column (name = "request_type", nullable = false) - Requestor = table.Column (name = "requestor", nullable = true) - SmallGroupId = table.Column (name = "small_group_id", nullable = false) - Text = table.Column (name = "request_text", nullable = false) - UpdatedDate = table.Column (name = "updated_date", nullable = false) - UserId = table.Column (name = "user_id", nullable = false) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_prayer_request", fun x -> upcast x.Id) |> ignore - table.ForeignKey ( - name = "fk_prayer_request_small_group_id", - column = (fun x -> upcast x.SmallGroupId), - principalSchema = "pt", - principalTable = "small_group", - principalColumn = "i", - onDelete = ReferentialAction.Cascade) - |> ignore - table.ForeignKey ( - name = "fk_prayer_request_user_id", - column = (fun x -> upcast x.UserId), - principalSchema = "pt", - principalTable = "pt_user", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore) - |> ignore - - migrationBuilder.CreateTable( - name = "user_small_group", - schema = "pt", - columns = (fun table -> - {| UserId = table.Column (name = "user_id", nullable = false) - SmallGroupId = table.Column (name = "small_group_id", nullable = false) - |}), - constraints = fun table -> - table.PrimaryKey ("pk_user_small_group", fun x -> upcast x) |> ignore - table.ForeignKey ( - name = "fk_user_small_group_small_group_id", - column = (fun x -> upcast x.SmallGroupId), - principalSchema = "pt", - principalTable = "small_group", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore - table.ForeignKey ( - name = "fk_user_small_group_user_id", - column = (fun x -> upcast x.UserId), - principalSchema = "pt", - principalTable = "pt_user", - principalColumn = "id", - onDelete = ReferentialAction.Cascade) - |> ignore) - |> ignore - - migrationBuilder.CreateIndex (name = "ix_list_preference_time_zone_id", schema = "pt", table = "list_preference", column = "time_zone_id") |> ignore - migrationBuilder.CreateIndex (name = "ix_member_small_group_id", schema = "pt", table = "member", column = "small_group_id") |> ignore - migrationBuilder.CreateIndex (name = "ix_prayer_request_small_group_id", schema = "pt", table = "prayer_request", column = "small_group_id") |> ignore - migrationBuilder.CreateIndex (name = "ix_prayer_request_user_id", schema = "pt", table = "prayer_request", column = "user_id") |> ignore - migrationBuilder.CreateIndex (name = "ix_small_group_church_id", schema = "pt", table = "small_group", column = "church_id") |> ignore - migrationBuilder.CreateIndex (name = "ix_user_small_group_small_group_id", schema = "pt", table = "user_small_group", column = "small_group_id") |> ignore - - override _.Down (migrationBuilder : MigrationBuilder) = - migrationBuilder.DropTable (name = "list_preference", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "member", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "prayer_request", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "user_small_group", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "time_zone", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "small_group", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "pt_user", schema = "pt") |> ignore - migrationBuilder.DropTable (name = "church", schema = "pt") |> ignore - - - override _.BuildTargetModel (modelBuilder : ModelBuilder) = - modelBuilder - .HasDefaultSchema("pt") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("City").HasColumnName("city").IsRequired() |> ignore - b.Property("HasVpsInterface").HasColumnName("has_vps_interface") |> ignore - b.Property("InterfaceAddress").HasColumnName("interface_address") |> ignore - b.Property("Name").HasColumnName("church_name").IsRequired() |> ignore - b.Property("State").HasColumnName("state").IsRequired().HasMaxLength(2) |> ignore - b.HasKey("Id") |> ignore - b.ToTable("church") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.Property("AsOfDateDisplay").HasColumnName("as_of_date_display").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("N").HasMaxLength(1) |> ignore - b.Property("DaysToExpire").HasColumnName("days_to_expire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore - b.Property("DaysToKeepNew").HasColumnName("days_to_keep_new").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore - b.Property("DefaultEmailType").HasColumnName("default_email_type").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H") |> ignore - b.Property("EmailFromAddress").HasColumnName("email_from_address").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore - b.Property("EmailFromName").HasColumnName("email_from_name").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore - b.Property("Fonts").HasColumnName("fonts").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Century Gothic,Tahoma,Luxi Sans,sans-serif") |> ignore - b.Property("GroupPassword").HasColumnName("group_password").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore - b.Property("HeadingColor").HasColumnName("heading_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("maroon") |> ignore - b.Property("HeadingFontSize").HasColumnName("heading_font_size").ValueGeneratedOnAdd().HasDefaultValue(16) |> ignore - b.Property("IsPublic").HasColumnName("is_public").ValueGeneratedOnAdd().HasDefaultValue(false) |> ignore - b.Property("LineColor").HasColumnName("line_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("navy") |> ignore - b.Property("LongTermUpdateWeeks").HasColumnName("long_term_update_weeks").ValueGeneratedOnAdd().HasDefaultValue(4) |> ignore - b.Property("PageSize").HasColumnName("page_size").IsRequired().ValueGeneratedOnAdd().HasDefaultValue(100) |> ignore - b.Property("RequestSort").HasColumnName("request_sort").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("D").HasMaxLength(1) |> ignore - b.Property("TextFontSize").HasColumnName("text_font_size").ValueGeneratedOnAdd().HasDefaultValue(12) |> ignore - b.Property("TimeZoneId").HasColumnName("time_zone_id").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("America/Denver") |> ignore - b.HasKey("SmallGroupId") |> ignore - b.HasIndex("TimeZoneId").HasDatabaseName "ix_list_preference_time_zone_id" |> ignore - b.ToTable("list_preference") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Email").HasColumnName("email").IsRequired() |> ignore - b.Property("Format").HasColumnName("email_format") |> ignore - b.Property("Name").HasColumnName("member_name").IsRequired() |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_member_small_group_id" |> ignore - b.ToTable("member") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("EnteredDate").HasColumnName("entered_date").IsRequired() |> ignore - b.Property("Expiration").HasColumnName("expiration").IsRequired().HasMaxLength 1 |> ignore - b.Property("NotifyChaplain").HasColumnName("notify_chaplain") |> ignore - b.Property("RequestType").HasColumnName("request_type").IsRequired().HasMaxLength 1 |> ignore - b.Property("Requestor").HasColumnName("requestor") |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.Property("Text").HasColumnName("request_text").IsRequired() |> ignore - b.Property("UpdatedDate").HasColumnName("updated_date") |> ignore - b.Property("UserId").HasColumnName("user_id") |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_prayer_request_small_group_id" |> ignore - b.HasIndex("UserId").HasDatabaseName "ix_prayer_request_user_id" |> ignore - b.ToTable("prayer_request") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("ChurchId").HasColumnName("church_id") |> ignore - b.Property("Name").HasColumnName("group_name").IsRequired() |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("ChurchId").HasDatabaseName "ix_small_group_church_id" |> ignore - b.ToTable("small_group") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Description").HasColumnName("description").IsRequired() |> ignore - b.Property("IsActive").HasColumnName("is_active") |> ignore - b.Property("SortOrder").HasColumnName("sort_order") |> ignore - b.HasKey("Id") |> ignore - b.ToTable("time_zone") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Email").HasColumnName("email").IsRequired() |> ignore - b.Property("FirstName").HasColumnName("first_name").IsRequired() |> ignore - b.Property("IsAdmin").HasColumnName("is_admin") |> ignore - b.Property("LastName").HasColumnName("last_name").IsRequired() |> ignore - b.Property("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore - b.Property("Salt").HasColumnName("salt") |> ignore - b.Property("LastSeen").HasColumnName("last_seen") |> ignore - b.HasKey("Id") |> ignore - b.ToTable("pt_user") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("UserId").HasColumnName("user_id") |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.HasKey("UserId", "SmallGroupId") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_user_small_group_small_group_id" |> ignore - b.ToTable("user_small_group") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup") - .WithOne("Preferences") - .HasForeignKey("PrayerTracker.Entities.ListPreferences", "SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.TimeZone", "TimeZone") - .WithMany() - .HasForeignKey("TimeZoneId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("Members") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("PrayerRequests") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.Church", "Church") - .WithMany("SmallGroups") - .HasForeignKey("ChurchId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("Users") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.User", "User") - .WithMany("SmallGroups") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore diff --git a/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs b/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs deleted file mode 100644 index 688e153..0000000 --- a/src/PrayerTracker.Data/Migrations/AppDbContextModelSnapshot.fs +++ /dev/null @@ -1,175 +0,0 @@ -namespace PrayerTracker.Migrations - -open System -open Microsoft.EntityFrameworkCore -open Microsoft.EntityFrameworkCore.Infrastructure -open Npgsql.EntityFrameworkCore.PostgreSQL.Metadata -open PrayerTracker -open PrayerTracker.Entities - -[)>] -type AppDbContextModelSnapshot () = - inherit ModelSnapshot () - - override _.BuildModel (modelBuilder : ModelBuilder) = - modelBuilder - .HasDefaultSchema("pt") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - |> ignore - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("City").HasColumnName("city").IsRequired() |> ignore - b.Property("HasVpsInterface").HasColumnName("has_vps_interface") |> ignore - b.Property("InterfaceAddress").HasColumnName("interface_address") |> ignore - b.Property("Name").HasColumnName("church_name").IsRequired() |> ignore - b.Property("State").HasColumnName("state").IsRequired().HasMaxLength(2) |> ignore - b.HasKey("Id") |> ignore - b.ToTable("church") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.Property("AsOfDateDisplay").HasColumnName("as_of_date_display").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("N").HasMaxLength(1) |> ignore - b.Property("DaysToExpire").HasColumnName("days_to_expire").ValueGeneratedOnAdd().HasDefaultValue(14) |> ignore - b.Property("DaysToKeepNew").HasColumnName("days_to_keep_new").ValueGeneratedOnAdd().HasDefaultValue(7) |> ignore - b.Property("DefaultEmailType").HasColumnName("default_email_type").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("H") |> ignore - b.Property("EmailFromAddress").HasColumnName("email_from_address").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("prayer@djs-consulting.com") |> ignore - b.Property("EmailFromName").HasColumnName("email_from_name").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("PrayerTracker") |> ignore - b.Property("Fonts").HasColumnName("fonts").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("Century Gothic,Tahoma,Luxi Sans,sans-serif") |> ignore - b.Property("GroupPassword").HasColumnName("group_password").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("") |> ignore - b.Property("HeadingColor").HasColumnName("heading_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("maroon") |> ignore - b.Property("HeadingFontSize").HasColumnName("heading_font_size").ValueGeneratedOnAdd().HasDefaultValue(16) |> ignore - b.Property("IsPublic").HasColumnName("is_public").ValueGeneratedOnAdd().HasDefaultValue(false) |> ignore - b.Property("LineColor").HasColumnName("line_color").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("navy") |> ignore - b.Property("LongTermUpdateWeeks").HasColumnName("long_term_update_weeks").ValueGeneratedOnAdd().HasDefaultValue(4) |> ignore - b.Property("PageSize").HasColumnName("page_size").IsRequired().ValueGeneratedOnAdd().HasDefaultValue(100) |> ignore - b.Property("RequestSort").HasColumnName("request_sort").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("D").HasMaxLength(1) |> ignore - b.Property("TextFontSize").HasColumnName("text_font_size").ValueGeneratedOnAdd().HasDefaultValue(12) |> ignore - b.Property("TimeZoneId").HasColumnName("time_zone_id").IsRequired().ValueGeneratedOnAdd().HasDefaultValue("America/Denver") |> ignore - b.HasKey("SmallGroupId") |> ignore - b.HasIndex("TimeZoneId").HasDatabaseName "ix_list_preference_time_zone_id" |> ignore - b.ToTable("list_preference") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Email").HasColumnName("email").IsRequired() |> ignore - b.Property("Format").HasColumnName("email_format") |> ignore - b.Property("Name").HasColumnName("member_name").IsRequired() |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_member_small_group_id" |> ignore - b.ToTable("member") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("EnteredDate").HasColumnName("entered_date").IsRequired() |> ignore - b.Property("Expiration").HasColumnName("expiration").IsRequired().HasMaxLength 1 |> ignore - b.Property("NotifyChaplain").HasColumnName("notify_chaplain") |> ignore - b.Property("RequestType").HasColumnName("request_type").IsRequired().HasMaxLength 1 |> ignore - b.Property("Requestor").HasColumnName("requestor") |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.Property("Text").HasColumnName("request_text").IsRequired() |> ignore - b.Property("UpdatedDate").HasColumnName("updated_date") |> ignore - b.Property("UserId").HasColumnName("user_id") |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_prayer_request_small_group_id" |> ignore - b.HasIndex("UserId").HasDatabaseName "ix_prayer_request_user_id" |> ignore - b.ToTable("prayer_request") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("ChurchId").HasColumnName("church_id") |> ignore - b.Property("Name").HasColumnName("group_name").IsRequired() |> ignore - b.HasKey("Id") |> ignore - b.HasIndex("ChurchId").HasDatabaseName "ix_small_group_church_id" |> ignore - b.ToTable("small_group") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Description").HasColumnName("description").IsRequired() |> ignore - b.Property("IsActive").HasColumnName("is_active") |> ignore - b.Property("SortOrder").HasColumnName("sort_order") |> ignore - b.HasKey("Id") |> ignore - b.ToTable("time_zone") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("Id").HasColumnName("id").ValueGeneratedOnAdd() |> ignore - b.Property("Email").HasColumnName("email").IsRequired() |> ignore - b.Property("FirstName").HasColumnName("first_name").IsRequired() |> ignore - b.Property("IsAdmin").HasColumnName("is_admin") |> ignore - b.Property("LastName").HasColumnName("last_name").IsRequired() |> ignore - b.Property("PasswordHash").HasColumnName("password_hash").IsRequired() |> ignore - b.Property("Salt").HasColumnName("salt") |> ignore - b.Property("LastSeen").HasColumnName("last_seen") |> ignore - b.HasKey("Id") |> ignore - b.ToTable("pt_user") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.Property("UserId").HasColumnName("user_id") |> ignore - b.Property("SmallGroupId").HasColumnName("small_group_id") |> ignore - b.HasKey("UserId", "SmallGroupId") |> ignore - b.HasIndex("SmallGroupId").HasDatabaseName "ix_user_small_group_small_group_id" |> ignore - b.ToTable("user_small_group") |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup") - .WithOne("Preferences") - .HasForeignKey("PrayerTracker.Entities.ListPreferences", "smallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.TimeZone", "TimeZone") - .WithMany() - .HasForeignKey("TimeZoneId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("Members") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("PrayerRequests") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.Church", "Church") - .WithMany("SmallGroups") - .HasForeignKey("ChurchId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore - - modelBuilder.Entity (typeof, fun b -> - b.HasOne("PrayerTracker.Entities.SmallGroup", "SmallGroup") - .WithMany("Users") - .HasForeignKey("SmallGroupId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore - b.HasOne("PrayerTracker.Entities.User", "User") - .WithMany("SmallGroups") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - |> ignore) - |> ignore diff --git a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj index fc50a81..a470e2d 100644 --- a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj +++ b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj @@ -7,18 +7,12 @@ - - - - - - - + diff --git a/src/PrayerTracker.Tests/Data/EntitiesTests.fs b/src/PrayerTracker.Tests/Data/EntitiesTests.fs index bce039f..67a3b85 100644 --- a/src/PrayerTracker.Tests/Data/EntitiesTests.fs +++ b/src/PrayerTracker.Tests/Data/EntitiesTests.fs @@ -118,8 +118,6 @@ let listPreferencesTests = Expect.isFalse mt.IsPublic "The isPublic flag should not have been set" Expect.equal (TimeZoneId.toString mt.TimeZoneId) "America/Denver" "The default time zone should have been America/Denver" - Expect.equal (TimeZoneId.toString mt.TimeZone.Id) "" - "The default preferences should have included an empty time zone" Expect.equal mt.PageSize 100 "The default page size should have been 100" Expect.equal mt.AsOfDateDisplay NoDisplay "The as-of date display should have been No Display" } @@ -135,7 +133,6 @@ let memberTests = Expect.equal mt.Name "" "The member name should have been blank" Expect.equal mt.Email "" "The member e-mail address should have been blank" Expect.isNone mt.Format "The preferred e-mail format should not exist" - Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one" } ] @@ -156,8 +153,6 @@ let prayerRequestTests = Expect.equal mt.Text "" "The request text should have been blank" Expect.isFalse mt.NotifyChaplain "The notify chaplain flag should not have been set" Expect.equal mt.Expiration Automatic "The expiration should have been Automatic" - Expect.equal mt.User.Id.Value Guid.Empty "The user should have been an empty one" - Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one" } test "isExpired always returns false for expecting requests" { PrayerRequest.isExpired (localDateNow ()) SmallGroup.empty @@ -294,13 +289,6 @@ let smallGroupTests = Expect.equal mt.Id.Value Guid.Empty "The small group ID should have been an empty GUID" Expect.equal mt.ChurchId.Value Guid.Empty "The church ID should have been an empty GUID" Expect.equal mt.Name "" "The name should have been blank" - Expect.equal mt.Church.Id.Value Guid.Empty "The church should have been an empty one" - Expect.isNotNull mt.Members "The members navigation property should not be null" - Expect.isEmpty mt.Members "There should be no members for an empty small group" - Expect.isNotNull mt.PrayerRequests "The prayer requests navigation property should not be null" - Expect.isEmpty mt.PrayerRequests "There should be no prayer requests for an empty small group" - Expect.isNotNull mt.Users "The users navigation property should not be null" - Expect.isEmpty mt.Users "There should be no users for an empty small group" } yield! testFixture withFakeClock [ "LocalTimeNow adjusts the time ahead of UTC", @@ -309,7 +297,6 @@ let smallGroupTests = { SmallGroup.empty with Preferences = { ListPreferences.empty with TimeZoneId = TimeZoneId "Europe/Berlin" } } - let tz = DateTimeZoneProviders.Tzdb["Europe/Berlin"] Expect.isGreaterThan (SmallGroup.localTimeNow clock grp) (now.InUtc().LocalDateTime) "UTC to Europe/Berlin should have added hours" "LocalTimeNow adjusts the time behind UTC", @@ -336,18 +323,6 @@ let smallGroupTests = } ] -[] -let timeZoneTests = - testList "TimeZone" [ - test "empty is as expected" { - let mt = TimeZone.empty - Expect.equal (TimeZoneId.toString mt.Id) "" "The time zone ID should have been blank" - Expect.equal mt.Description "" "The description should have been blank" - Expect.equal mt.SortOrder 0 "The sort order should have been zero" - Expect.isFalse mt.IsActive "The is-active flag should not have been set" - } - ] - [] let userTests = testList "User" [ @@ -359,9 +334,6 @@ let userTests = Expect.equal mt.Email "" "The e-mail address should have been blank" Expect.isFalse mt.IsAdmin "The is admin flag should not have been set" Expect.equal mt.PasswordHash "" "The password hash should have been blank" - Expect.isNone mt.Salt "The password salt should not exist" - Expect.isNotNull mt.SmallGroups "The small groups navigation property should not have been null" - Expect.isEmpty mt.SmallGroups "There should be no small groups for an empty user" } test "Name concatenates first and last names" { let user = { User.empty with FirstName = "Unit"; LastName = "Test" } @@ -376,7 +348,5 @@ let userSmallGroupTests = let mt = UserSmallGroup.empty Expect.equal mt.UserId.Value Guid.Empty "The user ID should have been an empty GUID" Expect.equal mt.SmallGroupId.Value Guid.Empty "The small group ID should have been an empty GUID" - Expect.equal mt.User.Id.Value Guid.Empty "The user should have been an empty one" - Expect.equal mt.SmallGroup.Id.Value Guid.Empty "The small group should have been an empty one" } ] diff --git a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj index fe2953b..13de8a4 100644 --- a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj +++ b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj @@ -16,7 +16,6 @@ - diff --git a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs index e52199f..418d3e8 100644 --- a/src/PrayerTracker.Tests/UI/ViewModelsTests.fs +++ b/src/PrayerTracker.Tests/UI/ViewModelsTests.fs @@ -543,9 +543,9 @@ let requestListTests = let curReqHtml = [ "
    " """
  • """ - "Zeb — zyx
  • " + "Zeb – zyx" """
  • """ - "Aaron — abc
" + "Aaron – abc" ] |> String.concat "" Expect.stringContains html curReqHtml """Expected HTML for "Current Requests" requests not found""" @@ -582,7 +582,7 @@ let requestListTests = |> String.concat "" Expect.stringContains html lstHeading "Expected HTML for the list heading not found" // spot check; without header test tests this exhaustively - Expect.stringContains html "Zeb — zyx" "Expected requests not found" + Expect.stringContains html "Zeb – zyx" "Expected requests not found" "AsHtml succeeds with short as-of date", fun reqList -> let htmlList = @@ -595,7 +595,7 @@ let requestListTests = let html = htmlList.AsHtml _s let expected = htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("d", null) - |> sprintf """Zeb — zyx  (as of %s)""" + |> sprintf """Zeb – zyx  (as of %s)""" // spot check; if one request has it, they all should Expect.stringContains html expected "Expected short as-of date not found" "AsHtml succeeds with long as-of date", @@ -610,7 +610,7 @@ let requestListTests = let html = htmlList.AsHtml _s let expected = htmlList.Requests[0].UpdatedDate.InUtc().Date.ToString ("D", null) - |> sprintf """Zeb — zyx  (as of %s)""" + |> sprintf """Zeb – zyx  (as of %s)""" // spot check; if one request has it, they all should Expect.stringContains html expected "Expected long as-of date not found" "AsText succeeds with no as-of date", diff --git a/src/PrayerTracker/App.fs b/src/PrayerTracker/App.fs index 007be90..d542f90 100644 --- a/src/PrayerTracker/App.fs +++ b/src/PrayerTracker/App.fs @@ -14,13 +14,12 @@ type RequestStartMiddleware (next : RequestDelegate) = open System open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration /// Module to hold configuration for the web app [] module Configure = - open Microsoft.Extensions.Configuration - /// Set up the configuration for the app let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) = cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath) @@ -39,7 +38,6 @@ module Configure = open System.IO open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Localization - open Microsoft.EntityFrameworkCore open Microsoft.Extensions.DependencyInjection open NeoSmart.Caching.Sqlite open NodaTime @@ -69,15 +67,6 @@ module Configure = let _ = svc.AddAntiforgery () let _ = svc.AddRouting () let _ = svc.AddSingleton SystemClock.Instance - - let config = svc.BuildServiceProvider().GetRequiredService () - let _ = - svc.AddDbContext ( - (fun options -> - options.UseNpgsql ( - config.GetConnectionString "PrayerTracker", fun o -> o.UseNodaTime () |> ignore) - |> ignore), - ServiceLifetime.Scoped, ServiceLifetime.Singleton) () open Giraffe @@ -194,10 +183,6 @@ module Configure = let _ = app.UseDeveloperExceptionPage () () else - try - use scope = app.ApplicationServices.GetRequiredService().CreateScope () - scope.ServiceProvider.GetService().Database.Migrate () - with _ -> () // om nom nom let _ = app.UseGiraffeErrorHandler errorHandler () @@ -219,34 +204,55 @@ module Configure = module App = open System.Text - open Microsoft.EntityFrameworkCore open Microsoft.Extensions.DependencyInjection + open Npgsql + open Npgsql.FSharp + open PrayerTracker.Entities let migratePasswords (app : IWebHost) = task { - use db = app.Services.GetService() - let! v1Users = db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NULL").ToListAsync () - for user in v1Users do + let config = app.Services.GetService () + use conn = new NpgsqlConnection (config.GetConnectionString "PrayerTracker") + do! conn.OpenAsync () + let! v1Users = + conn + |> Sql.existingConnection + |> Sql.query "SELECT id, password_hash FROM pt.pt_user WHERE salt IS NULL" + |> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash") + for userId, oldHash in v1Users do let pw = [| 254uy - yield! (Encoding.UTF8.GetBytes user.PasswordHash) + yield! (Encoding.UTF8.GetBytes oldHash) |] |> Convert.ToBase64String - db.UpdateEntry { user with PasswordHash = pw } - let! v1Count = db.SaveChangesAsync () - printfn $"Updated {v1Count} users with version 1 password" + let! _ = + conn + |> Sql.existingConnection + |> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ] + |> Sql.executeNonQueryAsync + () + printfn $"Updated {v1Users.Length} users with version 1 password" let! v2Users = - db.Users.FromSqlRaw("SELECT * FROM pt.pt_user WHERE salt IS NOT NULL").ToListAsync () - for user in v2Users do + conn + |> Sql.existingConnection + |> Sql.query "SELECT id, password_hash, salt FROM pt.pt_user WHERE salt IS NOT NULL" + |> Sql.executeAsync (fun row -> UserId (row.uuid "id"), row.string "password_hash", row.uuid "salt") + for userId, oldHash, salt in v2Users do let pw = [| 255uy - yield! (user.Salt.Value.ToByteArray ()) - yield! (Encoding.UTF8.GetBytes user.PasswordHash) + yield! (salt.ToByteArray ()) + yield! (Encoding.UTF8.GetBytes oldHash) |] |> Convert.ToBase64String - db.UpdateEntry { user with PasswordHash = pw } - let! v2Count = db.SaveChangesAsync () - printfn $"Updated {v2Count} users with version 2 password" + let! _ = + conn + |> Sql.existingConnection + |> Sql.query "UPDATE pt.pt_user SET password_hash = @hash WHERE id = @id" + |> Sql.parameters [ "@id", Sql.uuid userId.Value; "@hash", Sql.string pw ] + |> Sql.executeNonQueryAsync + () + printfn $"Updated {v2Users.Length} users with version 2 password" } |> Async.AwaitTask |> Async.RunSynchronously open System.IO diff --git a/src/PrayerTracker/Extensions.fs b/src/PrayerTracker/Extensions.fs index 1cf8409..2edaeb7 100644 --- a/src/PrayerTracker/Extensions.fs +++ b/src/PrayerTracker/Extensions.fs @@ -39,8 +39,7 @@ type ISession with with get () = this.GetObject Key.Session.currentUser |> Option.fromObject and set (v : User option) = match v with - | Some user -> - this.SetObject Key.Session.currentUser { user with PasswordHash = ""; SmallGroups = ResizeArray() } + | Some user -> this.SetObject Key.Session.currentUser { user with PasswordHash = "" } | None -> this.Remove Key.Session.currentUser /// Current messages for the session @@ -74,7 +73,6 @@ open System.Threading.Tasks open Giraffe open Microsoft.Extensions.Configuration open Npgsql -open PrayerTracker /// Extensions on the ASP.NET Core HTTP context type HttpContext with @@ -87,9 +85,6 @@ type HttpContext with return conn }) - /// The EF Core database context (via DI) - member this.Db = this.GetService () - /// The PostgreSQL connection (configured via DI) member this.Conn = backgroundTask { return! this.LazyConn.Force () diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index 4dd8ce3..683807d 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -126,7 +126,7 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task | Ok req -> let s = Views.I18N.localizer.Force () let! conn = ctx.Conn - do! PrayerRequests.updateExpiration { req with Expiration = Forced } conn + do! PrayerRequests.updateExpiration { req with Expiration = Forced } false conn addInfo ctx s["Successfully {0} prayer request", s["Expired"].Value.ToLower ()] return! redirectTo false "/prayer-requests" next ctx | Result.Error e -> return! e @@ -177,16 +177,16 @@ let lists : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx /// - OR - /// GET /prayer-requests?search=[search-query] let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task { - // TODO: stopped here let group = ctx.Session.CurrentGroup.Value let pageNbr = match ctx.GetQueryStringValue "page" with | Ok pg -> match Int32.TryParse pg with true, p -> p | false, _ -> 1 | Result.Error _ -> 1 let! model = backgroundTask { + let! conn = ctx.Conn match ctx.GetQueryStringValue "search" with | Ok search -> - let! reqs = ctx.Db.SearchRequestsForSmallGroup group search pageNbr + let! reqs = PrayerRequests.searchForGroup group search pageNbr conn return { MaintainRequests.empty with Requests = reqs @@ -194,7 +194,14 @@ let maintain onlyActive : HttpHandler = requireAccess [ User ] >=> fun next ctx PageNbr = Some pageNbr } | Result.Error _ -> - let! reqs = ctx.Db.AllRequestsForSmallGroup group ctx.Clock None onlyActive pageNbr + let! reqs = + PrayerRequests.forGroup + { SmallGroup = group + Clock = ctx.Clock + ListDate = None + ActiveOnly = onlyActive + PageNumber = pageNbr + } conn return { MaintainRequests.empty with Requests = reqs @@ -221,9 +228,9 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas let requestId = PrayerRequestId reqId match! findRequest ctx requestId with | Ok req -> - let s = Views.I18N.localizer.Force () - ctx.Db.UpdateEntry { req with Expiration = Automatic; UpdatedDate = ctx.Now } - let! _ = ctx.Db.SaveChangesAsync () + let s = Views.I18N.localizer.Force () + let! conn = ctx.Conn + do! PrayerRequests.updateExpiration { req with Expiration = Automatic; UpdatedDate = ctx.Now } true conn addInfo ctx s["Successfully {0} prayer request", s["Restored"].Value.ToLower ()] return! redirectTo false "/prayer-requests" next ctx | Result.Error e -> return! e @@ -235,41 +242,42 @@ open System.Threading.Tasks let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task { match! ctx.TryBindFormAsync () with | Ok model -> - let! req = - if model.IsNew then - Task.FromResult (Some { PrayerRequest.empty with Id = (Guid.NewGuid >> PrayerRequestId) () }) - else ctx.Db.TryRequestById (idFromShort PrayerRequestId model.RequestId) + let group = ctx.Session.CurrentGroup.Value + let! conn = ctx.Conn + let! req = + if model.IsNew then + { PrayerRequest.empty with + Id = (Guid.NewGuid >> PrayerRequestId) () + SmallGroupId = group.Id + UserId = ctx.User.UserId.Value + } + |> (Some >> Task.FromResult) + else PrayerRequests.tryById (idFromShort PrayerRequestId model.RequestId) conn match req with - | Some pr -> - let upd8 = + | Some pr when pr.SmallGroupId = group.Id -> + let now = SmallGroup.localDateNow ctx.Clock group + let updated = { pr with RequestType = PrayerRequestType.fromCode model.RequestType Requestor = match model.Requestor with Some x when x.Trim () = "" -> None | x -> x Text = ckEditorToText model.Text Expiration = Expiration.fromCode model.Expiration } - let group = ctx.Session.CurrentGroup.Value - let now = SmallGroup.localDateNow ctx.Clock group - match model.IsNew with - | true -> - let dt = - (defaultArg (parseListDate model.EnteredDate) now) - .AtStartOfDayInZone(SmallGroup.timeZone group) - .ToInstant() - { upd8 with - SmallGroupId = group.Id - UserId = ctx.User.UserId.Value - EnteredDate = dt - UpdatedDate = dt - } - | false when defaultArg model.SkipDateUpdate false -> upd8 - | false -> { upd8 with UpdatedDate = ctx.Now } - |> if model.IsNew then ctx.Db.AddEntry else ctx.Db.UpdateEntry - let! _ = ctx.Db.SaveChangesAsync () - let s = Views.I18N.localizer.Force () - let act = if model.IsNew then "Added" else "Updated" + |> function + | it when model.IsNew -> + let dt = + (defaultArg (parseListDate model.EnteredDate) now) + .AtStartOfDayInZone(SmallGroup.timeZone group) + .ToInstant() + { it with EnteredDate = dt; UpdatedDate = dt } + | it when defaultArg model.SkipDateUpdate false -> it + | it -> { it with UpdatedDate = ctx.Now } + do! PrayerRequests.save updated conn + let s = Views.I18N.localizer.Force () + let act = if model.IsNew then "Added" else "Updated" addInfo ctx s["Successfully {0} prayer request", s[act].Value.ToLower ()] return! redirectTo false "/prayer-requests" next ctx + | Some _ | None -> return! fourOhFour ctx | Result.Error e -> return! bindError e next ctx } diff --git a/src/PrayerTracker/PrayerTracker.fsproj b/src/PrayerTracker/PrayerTracker.fsproj index 0b02431..ffe1198 100644 --- a/src/PrayerTracker/PrayerTracker.fsproj +++ b/src/PrayerTracker/PrayerTracker.fsproj @@ -28,9 +28,7 @@ - -