From 81169ffd2f8e30cc22126e31fa363fe27811b5bf Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Fri, 4 Sep 2020 18:06:21 -0700 Subject: [PATCH] Adjust schema to accomodate moved account property --- DB/Sources/DB/Content/AccountResult.swift | 17 ++++ DB/Sources/DB/Content/ContentDatabase.swift | 5 +- DB/Sources/DB/Content/StatusResult.swift | 32 +++++-- DB/Sources/DB/Content/StoredAccount.swift | 65 +++++++++++++ DB/Sources/DB/Content/StoredStatus.swift | 31 +++---- .../DB/Extensions/Account+Extensions.swift | 43 ++++++++- .../DB/Extensions/Status+ Extensions.swift | 6 +- .../DB/Extensions/Timeline+Extensions.swift | 2 +- .../Sources/Mastodon/Entities/Account.swift | 93 ++++++++++++++++++- .../Sources/Mastodon/Entities/Status.swift | 1 - 10 files changed, 258 insertions(+), 37 deletions(-) create mode 100644 DB/Sources/DB/Content/AccountResult.swift create mode 100644 DB/Sources/DB/Content/StoredAccount.swift diff --git a/DB/Sources/DB/Content/AccountResult.swift b/DB/Sources/DB/Content/AccountResult.swift new file mode 100644 index 0000000..c834431 --- /dev/null +++ b/DB/Sources/DB/Content/AccountResult.swift @@ -0,0 +1,17 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct AccountResult: Codable, Hashable, FetchableRecord { + let account: StoredAccount + let moved: StoredAccount? +} + +extension QueryInterfaceRequest where RowDecoder == StoredAccount { + var accountResultRequest: AnyFetchRequest { + AnyFetchRequest(including(optional: StoredAccount.moved)) + .asRequest(of: AccountResult.self) + } +} diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 5cb7c31..2fe8f1d 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -193,7 +193,7 @@ private extension ContentDatabase { var migrator = DatabaseMigrator() migrator.registerMigration("createStatuses") { db in - try db.create(table: "account", ifNotExists: true) { t in + try db.create(table: "storedAccount", ifNotExists: true) { t in t.column("id", .text).notNull().primaryKey(onConflict: .replace) t.column("username", .text).notNull() t.column("acct", .text).notNull() @@ -213,13 +213,14 @@ private extension ContentDatabase { t.column("emojis", .blob).notNull() t.column("bot", .boolean).notNull() t.column("discoverable", .boolean) + t.column("movedId", .text).indexed().references("storedAccount", column: "id") } try db.create(table: "storedStatus", ifNotExists: true) { t in t.column("id", .text).notNull().primaryKey(onConflict: .replace) t.column("uri", .text).notNull() t.column("createdAt", .datetime).notNull() - t.column("accountId", .text).indexed().notNull().references("account", column: "id") + t.column("accountId", .text).indexed().notNull().references("storedAccount", column: "id") t.column("content", .text).notNull() t.column("visibility", .text).notNull() t.column("sensitive", .boolean).notNull() diff --git a/DB/Sources/DB/Content/StatusResult.swift b/DB/Sources/DB/Content/StatusResult.swift index f028eaf..b3fbf5c 100644 --- a/DB/Sources/DB/Content/StatusResult.swift +++ b/DB/Sources/DB/Content/StatusResult.swift @@ -5,17 +5,33 @@ import GRDB import Mastodon struct StatusResult: Codable, Hashable, FetchableRecord { - let account: Account + let account: StoredAccount + let accountMoved: StoredAccount? let status: StoredStatus - let reblogAccount: Account? + let reblogAccount: StoredAccount? + let reblogAccountMoved: StoredAccount? let reblog: StoredStatus? } -extension QueryInterfaceRequest where RowDecoder == StoredStatus { - var statusResultRequest: QueryInterfaceRequest { - including(required: StoredStatus.account) - .including(optional: StoredStatus.reblogAccount) - .including(optional: StoredStatus.reblog) - .asRequest(of: StatusResult.self) +extension StatusResult { + var accountResult: AccountResult { + AccountResult(account: account, moved: accountMoved) + } + + var reblogAccountResult: AccountResult? { + guard let reblogAccount = reblogAccount else { return nil } + + return AccountResult(account: reblogAccount, moved: reblogAccountMoved) + } +} + +extension QueryInterfaceRequest where RowDecoder == StoredStatus { + var statusResultRequest: AnyFetchRequest { + AnyFetchRequest(including(required: StoredStatus.account) + .including(optional: StoredStatus.accountMoved) + .including(optional: StoredStatus.reblogAccount) + .including(optional: StoredStatus.reblogAccountMoved) + .including(optional: StoredStatus.reblog)) + .asRequest(of: StatusResult.self) } } diff --git a/DB/Sources/DB/Content/StoredAccount.swift b/DB/Sources/DB/Content/StoredAccount.swift new file mode 100644 index 0000000..a82def9 --- /dev/null +++ b/DB/Sources/DB/Content/StoredAccount.swift @@ -0,0 +1,65 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct StoredAccount: Codable, Hashable { + let id: String + let username: String + let acct: String + let displayName: String + let locked: Bool + let createdAt: Date + let followersCount: Int + let followingCount: Int + let statusesCount: Int + let note: HTML + let url: URL + let avatar: URL + let avatarStatic: URL + let header: URL + let headerStatic: URL + let fields: [Account.Field] + let emojis: [Emoji] + let bot: Bool + let discoverable: Bool + let movedId: String? +} + +extension StoredAccount: FetchableRecord, PersistableRecord { + static func databaseJSONDecoder(for column: String) -> JSONDecoder { + MastodonDecoder() + } + + static func databaseJSONEncoder(for column: String) -> JSONEncoder { + MastodonEncoder() + } +} + +extension StoredAccount { + static let moved = belongsTo(StoredAccount.self, key: "moved") + + init(account: Account) { + id = account.id + username = account.username + acct = account.acct + displayName = account.displayName + locked = account.locked + createdAt = account.createdAt + followersCount = account.followersCount + followingCount = account.followingCount + statusesCount = account.statusesCount + note = account.note + url = account.url + avatar = account.avatar + avatarStatic = account.avatarStatic + header = account.header + headerStatic = account.headerStatic + fields = account.fields + emojis = account.emojis + bot = account.bot + discoverable = account.discoverable + movedId = account.moved?.id + } +} diff --git a/DB/Sources/DB/Content/StoredStatus.swift b/DB/Sources/DB/Content/StoredStatus.swift index ca7f893..8303a9e 100644 --- a/DB/Sources/DB/Content/StoredStatus.swift +++ b/DB/Sources/DB/Content/StoredStatus.swift @@ -47,8 +47,19 @@ extension StoredStatus: FetchableRecord, PersistableRecord { } extension StoredStatus { - static let account = belongsTo(Account.self, key: "account") - static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount") + static let account = belongsTo(StoredAccount.self, key: "account", using: ForeignKey([Column("accountId")])) + static let accountMoved = hasOne(StoredAccount.self, + through: Self.account, + using: StoredAccount.moved, + key: "accountMoved") + static let reblogAccount = hasOne(StoredAccount.self, + through: Self.reblog, + using: Self.account, + key: "reblogAccount") + static let reblogAccountMoved = hasOne(StoredAccount.self, + through: Self.reblogAccount, + using: StoredAccount.moved, + key: "reblogAccountMoved") static let reblog = belongsTo(StoredStatus.self, key: "reblog") static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")])) .filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue) @@ -63,23 +74,11 @@ extension StoredStatus { through: descendantJoins, using: StatusContextJoin.status) - var account: QueryInterfaceRequest { - request(for: Self.account) - } - - var reblogAccount: QueryInterfaceRequest { - request(for: Self.reblogAccount) - } - - var reblog: QueryInterfaceRequest { - request(for: Self.reblog) - } - - var ancestors: QueryInterfaceRequest { + var ancestors: AnyFetchRequest { request(for: Self.ancestors).statusResultRequest } - var descendants: QueryInterfaceRequest { + var descendants: AnyFetchRequest { request(for: Self.descendants).statusResultRequest } diff --git a/DB/Sources/DB/Extensions/Account+Extensions.swift b/DB/Sources/DB/Extensions/Account+Extensions.swift index 74fdd29..7ecde97 100644 --- a/DB/Sources/DB/Extensions/Account+Extensions.swift +++ b/DB/Sources/DB/Extensions/Account+Extensions.swift @@ -4,12 +4,45 @@ import Foundation import GRDB import Mastodon -extension Account: FetchableRecord, PersistableRecord { - public static func databaseJSONDecoder(for column: String) -> JSONDecoder { - MastodonDecoder() +extension Account { + func save(_ db: Database) throws { + if let moved = moved { + try StoredAccount(account: moved).save(db) + } + + try StoredAccount(account: self).save(db) } - public static func databaseJSONEncoder(for column: String) -> JSONEncoder { - MastodonEncoder() + convenience init(accountResult: AccountResult) { + var moved: Account? + + if let movedResult = accountResult.moved { + moved = Self(storedAccount: movedResult, moved: nil) + } + + self.init(storedAccount: accountResult.account, moved: moved) + } + + convenience init(storedAccount: StoredAccount, moved: Account?) { + self.init(id: storedAccount.id, + username: storedAccount.username, + acct: storedAccount.acct, + displayName: storedAccount.displayName, + locked: storedAccount.locked, + createdAt: storedAccount.createdAt, + followersCount: storedAccount.followersCount, + followingCount: storedAccount.followingCount, + statusesCount: storedAccount.statusesCount, + note: storedAccount.note, + url: storedAccount.url, + avatar: storedAccount.avatar, + avatarStatic: storedAccount.avatarStatic, + header: storedAccount.header, + headerStatic: storedAccount.headerStatic, + fields: storedAccount.fields, + emojis: storedAccount.emojis, + bot: storedAccount.bot, + discoverable: storedAccount.discoverable, + moved: moved) } } diff --git a/DB/Sources/DB/Extensions/Status+ Extensions.swift b/DB/Sources/DB/Extensions/Status+ Extensions.swift index 5cbacc2..04b627c 100644 --- a/DB/Sources/DB/Extensions/Status+ Extensions.swift +++ b/DB/Sources/DB/Extensions/Status+ Extensions.swift @@ -19,11 +19,11 @@ extension Status { convenience init(statusResult: StatusResult) { var reblog: Status? - if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount { - reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil) + if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccountResult { + reblog = Status(storedStatus: reblogResult, account: Account(accountResult: reblogAccount), reblog: nil) } - self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog) + self.init(storedStatus: statusResult.status, account: Account(accountResult: statusResult.accountResult), reblog: reblog) } convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) { diff --git a/DB/Sources/DB/Extensions/Timeline+Extensions.swift b/DB/Sources/DB/Extensions/Timeline+Extensions.swift index cf4a9a9..593aa3c 100644 --- a/DB/Sources/DB/Extensions/Timeline+Extensions.swift +++ b/DB/Sources/DB/Extensions/Timeline+Extensions.swift @@ -41,7 +41,7 @@ extension Timeline { using: TimelineStatusJoin.status) .order(Column("createdAt").desc) - var statuses: QueryInterfaceRequest { + var statuses: AnyFetchRequest { request(for: Self.statuses).statusResultRequest } } diff --git a/Mastodon/Sources/Mastodon/Entities/Account.swift b/Mastodon/Sources/Mastodon/Entities/Account.swift index c9fe844..3d492e1 100644 --- a/Mastodon/Sources/Mastodon/Entities/Account.swift +++ b/Mastodon/Sources/Mastodon/Entities/Account.swift @@ -2,7 +2,7 @@ import Foundation -public struct Account: Codable, Hashable { +public final class Account: Codable, Identifiable { public struct Field: Codable, Hashable { public let name: String public let value: HTML @@ -28,4 +28,95 @@ public struct Account: Codable, Hashable { public let emojis: [Emoji] @DecodableDefault.False public private(set) var bot: Bool @DecodableDefault.False public private(set) var discoverable: Bool + public var moved: Account? + + public init(id: String, + username: String, + acct: String, + displayName: String, + locked: Bool, + createdAt: Date, + followersCount: Int, + followingCount: Int, + statusesCount: Int, + note: HTML, + url: URL, + avatar: URL, + avatarStatic: URL, + header: URL, + headerStatic: URL, + fields: [Account.Field], + emojis: [Emoji], + bot: Bool, + discoverable: Bool, + moved: Account?) { + self.id = id + self.username = username + self.acct = acct + self.displayName = displayName + self.locked = locked + self.createdAt = createdAt + self.followersCount = followersCount + self.followingCount = followingCount + self.statusesCount = statusesCount + self.note = note + self.url = url + self.avatar = avatar + self.avatarStatic = avatarStatic + self.header = header + self.headerStatic = headerStatic + self.fields = fields + self.emojis = emojis + self.bot = bot + self.discoverable = discoverable + self.moved = moved + } +} + +extension Account: Hashable { + public static func == (lhs: Account, rhs: Account) -> Bool { + return lhs.id == rhs.id && + lhs.username == rhs.username && + lhs.acct == rhs.acct && + lhs.displayName == rhs.displayName && + lhs.locked == rhs.locked && + lhs.createdAt == rhs.createdAt && + lhs.followersCount == rhs.followersCount && + lhs.followingCount == rhs.followingCount && + lhs.statusesCount == rhs.statusesCount && + lhs.note == rhs.note && + lhs.url == rhs.url && + lhs.avatar == rhs.avatar && + lhs.avatarStatic == rhs.avatarStatic && + lhs.header == rhs.header && + lhs.headerStatic == rhs.headerStatic && + lhs.fields == rhs.fields && + lhs.emojis == rhs.emojis && + lhs._bot == rhs._bot && + lhs._discoverable == rhs._discoverable && + lhs.moved == rhs.moved + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(username) + hasher.combine(acct) + hasher.combine(displayName) + hasher.combine(locked) + hasher.combine(createdAt) + hasher.combine(followersCount) + hasher.combine(followingCount) + hasher.combine(statusesCount) + hasher.combine(note) + hasher.combine(url) + hasher.combine(avatar) + hasher.combine(avatarStatic) + hasher.combine(header) + hasher.combine(headerStatic) + hasher.combine(fields) + hasher.combine(emojis) + hasher.combine(bot) + hasher.combine(discoverable) + hasher.combine(moved) + } } diff --git a/Mastodon/Sources/Mastodon/Entities/Status.swift b/Mastodon/Sources/Mastodon/Entities/Status.swift index 35b7bc8..3a05bff 100644 --- a/Mastodon/Sources/Mastodon/Entities/Status.swift +++ b/Mastodon/Sources/Mastodon/Entities/Status.swift @@ -43,7 +43,6 @@ public final class Status: Codable, Identifiable { @DecodableDefault.False public private(set) var bookmarked: Bool public let pinned: Bool? - // Xcode-generated memberwise initializer public init( id: String, uri: String,