diff --git a/src/PrayerTracker.Data/Access.fs b/src/PrayerTracker.Data/Access.fs index 4d77c41..fff7867 100644 --- a/src/PrayerTracker.Data/Access.fs +++ b/src/PrayerTracker.Data/Access.fs @@ -5,6 +5,31 @@ open Npgsql open Npgsql.FSharp open PrayerTracker.Entities +/// Table names +[] +module Table = + + /// The church table + [] + let Church = "church" + + /// The small group table + [] + let Group = "small_group" + + /// The small group member table + [] + let Member = "member" + + /// The prayer request table + [] + let Request = "prayer_request" + + /// The user table + [] + let User = "pt_user" + + /// Helper functions for the PostgreSQL data implementation [] module private Helpers = @@ -100,6 +125,7 @@ module private Helpers = IsAdmin = row.bool "is_admin" PasswordHash = row.string "password_hash" LastSeen = row.fieldValueOrNone "last_seen" + SmallGroups = [] } diff --git a/src/PrayerTracker.Data/Entities.fs b/src/PrayerTracker.Data/Entities.fs index 610a2fd..3182857 100644 --- a/src/PrayerTracker.Data/Entities.fs +++ b/src/PrayerTracker.Data/Entities.fs @@ -127,6 +127,15 @@ type RequestSort = | _ -> invalidArg "code" $"Unknown code {code}" +/// Type for a time zone ID +type TimeZoneId = + | TimeZoneId of string + + override this.ToString() = + match this with + | TimeZoneId it -> it + + open System /// PK type for the Church entity @@ -173,15 +182,6 @@ type SmallGroupId = | SmallGroupId guid -> guid -/// PK type for the TimeZone entity -type TimeZoneId = - | TimeZoneId of string - - override this.ToString() = - match this with - | TimeZoneId it -> it - - /// PK type for the User entity type UserId = | UserId of Guid @@ -523,6 +523,9 @@ type User = /// The last time the user was seen (set whenever the user is loaded into a session) LastSeen: Instant option + + /// The small groups to which this user is authorized + SmallGroups: SmallGroupId list } /// The full name of the user @@ -536,7 +539,8 @@ type User = Email = "" IsAdmin = false PasswordHash = "" - LastSeen = None } + LastSeen = None + SmallGroups = [] } /// Cross-reference between user and small group diff --git a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj index a10c238..4007c8c 100644 --- a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj +++ b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/PrayerTracker.MigrateV9/PrayerTracker.MigrateV9.fsproj b/src/PrayerTracker.MigrateV9/PrayerTracker.MigrateV9.fsproj new file mode 100644 index 0000000..a3fda13 --- /dev/null +++ b/src/PrayerTracker.MigrateV9/PrayerTracker.MigrateV9.fsproj @@ -0,0 +1,20 @@ + + + + Exe + net9.0 + + + + + + + + + + + + + + + diff --git a/src/PrayerTracker.MigrateV9/Program.fs b/src/PrayerTracker.MigrateV9/Program.fs new file mode 100644 index 0000000..35fd6a3 --- /dev/null +++ b/src/PrayerTracker.MigrateV9/Program.fs @@ -0,0 +1,141 @@ + +open NodaTime +open Npgsql.FSharp +open PrayerTracker.Data +open PrayerTracker.Entities + +module PgMappings = + /// Map a row to a Church instance + let mapToChurch (row : RowReader) = + { Id = ChurchId (row.uuid "id") + Name = row.string "church_name" + City = row.string "city" + State = row.string "state" + HasVpsInterface = row.bool "has_vps_interface" + InterfaceAddress = row.stringOrNone "interface_address" + } + + /// Map a row to a ListPreferences instance + let mapToListPreferences (row : RowReader) = + { SmallGroupId = SmallGroupId (row.uuid "small_group_id") + DaysToKeepNew = row.int "days_to_keep_new" + DaysToExpire = row.int "days_to_expire" + LongTermUpdateWeeks = row.int "long_term_update_weeks" + EmailFromName = row.string "email_from_name" + EmailFromAddress = row.string "email_from_address" + Fonts = row.string "fonts" + HeadingColor = row.string "heading_color" + LineColor = row.string "line_color" + HeadingFontSize = row.int "heading_font_size" + TextFontSize = row.int "text_font_size" + GroupPassword = row.string "group_password" + IsPublic = row.bool "is_public" + PageSize = row.int "page_size" + TimeZoneId = TimeZoneId (row.string "time_zone_id") + RequestSort = RequestSort.Parse (row.string "request_sort") + DefaultEmailType = EmailFormat.Parse (row.string "default_email_type") + AsOfDateDisplay = AsOfDateDisplay.Parse (row.string "as_of_date_display") + } + + /// Map a row to a Member instance + let mapToMember (row : RowReader) = + { Id = MemberId (row.uuid "id") + SmallGroupId = SmallGroupId (row.uuid "small_group_id") + Name = row.string "member_name" + Email = row.string "email" + Format = row.stringOrNone "email_format" |> Option.map EmailFormat.Parse + } + + /// Map a row to a Prayer Request instance + let mapToPrayerRequest (row : RowReader) = + { Id = PrayerRequestId (row.uuid "id") + UserId = UserId (row.uuid "user_id") + SmallGroupId = SmallGroupId (row.uuid "small_group_id") + EnteredDate = row.fieldValue "entered_date" + UpdatedDate = row.fieldValue "updated_date" + Requestor = row.stringOrNone "requestor" + Text = row.string "request_text" + NotifyChaplain = row.bool "notify_chaplain" + RequestType = PrayerRequestType.Parse (row.string "request_type") + Expiration = Expiration.Parse (row.string "expiration") + } + + /// Map a row to a Small Group instance + let mapToSmallGroup (row : RowReader) = + { Id = SmallGroupId (row.uuid "id") + ChurchId = ChurchId (row.uuid "church_id") + Name = row.string "group_name" + Preferences = ListPreferences.Empty + } + + /// Map a row to a Small Group instance with populated list preferences + let mapToSmallGroupWithPreferences (row : RowReader) = + { mapToSmallGroup row with + Preferences = mapToListPreferences row + } + + /// Map a row to a User instance + let mapToUser (row : RowReader) = + { Id = UserId (row.uuid "id") + FirstName = row.string "first_name" + LastName = row.string "last_name" + Email = row.string "email" + IsAdmin = row.bool "is_admin" + PasswordHash = row.string "password_hash" + LastSeen = row.fieldValueOrNone "last_seen" + SmallGroups = [] + } + +// TODO: Configure PostgreSQL and SQLite connections + +task { + + let source = BitBadger.Documents.Postgres.Configuration.dataSource () + + let! churches = + Sql.fromDataSource source + |> Sql.query "SELECT * FROM pt.church" + |> Sql.executeAsync PgMappings.mapToChurch + for church in churches do + do! BitBadger.Documents.Sqlite.Document.insert Table.Church church + printfn "Migrated %d churches" churches.Length + + let! groups = + Sql.fromDataSource source + |> Sql.query "SELECT sg.*, lp.* FROM pt.small_group sg + INNER JOIN pt.list_preference lp ON lp.small_group_id = sg.id" + |> Sql.executeAsync PgMappings.mapToSmallGroupWithPreferences + for group in groups do + do! BitBadger.Documents.Sqlite.Document.insert Table.Group group + printfn "Migrated %d groups" groups.Length + + let! members = + Sql.fromDataSource source + |> Sql.query "SELECT * from pt.member" + |> Sql.executeAsync PgMappings.mapToMember + for mbr in members do + do! BitBadger.Documents.Sqlite.Document.insert Table.Member mbr + printfn "Migrated %d members" members.Length + + let! requests = + Sql.fromDataSource source + |> Sql.query "SELECT * from pt.prayer_request" + |> Sql.executeAsync PgMappings.mapToPrayerRequest + for request in requests do + do! BitBadger.Documents.Sqlite.Document.insert Table.Request request + printfn "Migrated %d requests" requests.Length + + let! users = + Sql.fromDataSource source + |> Sql.query "SELECT * FROM pt.pt_user" + |> Sql.executeAsync PgMappings.mapToUser + for user in users do + let! groups = + Sql.fromDataSource source + |> Sql.query "SELECT small_group_id FROM pt.user_small_group WHERE user_id = :user_id" + |> Sql.parameters [ ":user_id", Sql.uuid user.Id.Value ] + |> Sql.executeAsync (fun row -> (row.uuid >> SmallGroupId) "small_group_id") + do! BitBadger.Documents.Sqlite.Document.insert Table.User { user with SmallGroups = groups } + printfn "Migrated %d users" users.Length + +} |> Async.AwaitTask |> Async.RunSynchronously diff --git a/src/PrayerTracker.sln b/src/PrayerTracker.sln index 07c8f9b..2ff4142 100644 --- a/src/PrayerTracker.sln +++ b/src/PrayerTracker.sln @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PrayerTracker.MigrateV9", "PrayerTracker.MigrateV9\PrayerTracker.MigrateV9.fsproj", "{CE7C5972-AC9A-44A8-8265-771483FD87DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B5BA107-9BDA-4A1D-A9AF-AFEE6BF12270}.Release|Any CPU.Build.0 = Release|Any CPU + {CE7C5972-AC9A-44A8-8265-771483FD87DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE7C5972-AC9A-44A8-8265-771483FD87DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE7C5972-AC9A-44A8-8265-771483FD87DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE7C5972-AC9A-44A8-8265-771483FD87DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE