Adjust schema to accomodate moved account property

This commit is contained in:
Justin Mazzocchi 2020-09-04 18:06:21 -07:00
parent 4b0f896f64
commit 81169ffd2f
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
10 changed files with 258 additions and 37 deletions

View file

@ -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<AccountResult> {
AnyFetchRequest(including(optional: StoredAccount.moved))
.asRequest(of: AccountResult.self)
}
}

View file

@ -193,7 +193,7 @@ private extension ContentDatabase {
var migrator = DatabaseMigrator() var migrator = DatabaseMigrator()
migrator.registerMigration("createStatuses") { db in 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("id", .text).notNull().primaryKey(onConflict: .replace)
t.column("username", .text).notNull() t.column("username", .text).notNull()
t.column("acct", .text).notNull() t.column("acct", .text).notNull()
@ -213,13 +213,14 @@ private extension ContentDatabase {
t.column("emojis", .blob).notNull() t.column("emojis", .blob).notNull()
t.column("bot", .boolean).notNull() t.column("bot", .boolean).notNull()
t.column("discoverable", .boolean) t.column("discoverable", .boolean)
t.column("movedId", .text).indexed().references("storedAccount", column: "id")
} }
try db.create(table: "storedStatus", ifNotExists: true) { t in try db.create(table: "storedStatus", ifNotExists: true) { t in
t.column("id", .text).notNull().primaryKey(onConflict: .replace) t.column("id", .text).notNull().primaryKey(onConflict: .replace)
t.column("uri", .text).notNull() t.column("uri", .text).notNull()
t.column("createdAt", .datetime).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("content", .text).notNull()
t.column("visibility", .text).notNull() t.column("visibility", .text).notNull()
t.column("sensitive", .boolean).notNull() t.column("sensitive", .boolean).notNull()

View file

@ -5,17 +5,33 @@ import GRDB
import Mastodon import Mastodon
struct StatusResult: Codable, Hashable, FetchableRecord { struct StatusResult: Codable, Hashable, FetchableRecord {
let account: Account let account: StoredAccount
let accountMoved: StoredAccount?
let status: StoredStatus let status: StoredStatus
let reblogAccount: Account? let reblogAccount: StoredAccount?
let reblogAccountMoved: StoredAccount?
let reblog: StoredStatus? let reblog: StoredStatus?
} }
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 { extension QueryInterfaceRequest where RowDecoder == StoredStatus {
var statusResultRequest: QueryInterfaceRequest<StatusResult> { var statusResultRequest: AnyFetchRequest<StatusResult> {
including(required: StoredStatus.account) AnyFetchRequest(including(required: StoredStatus.account)
.including(optional: StoredStatus.accountMoved)
.including(optional: StoredStatus.reblogAccount) .including(optional: StoredStatus.reblogAccount)
.including(optional: StoredStatus.reblog) .including(optional: StoredStatus.reblogAccountMoved)
.including(optional: StoredStatus.reblog))
.asRequest(of: StatusResult.self) .asRequest(of: StatusResult.self)
} }
} }

View file

@ -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
}
}

View file

@ -47,8 +47,19 @@ extension StoredStatus: FetchableRecord, PersistableRecord {
} }
extension StoredStatus { extension StoredStatus {
static let account = belongsTo(Account.self, key: "account") static let account = belongsTo(StoredAccount.self, key: "account", using: ForeignKey([Column("accountId")]))
static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount") 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 reblog = belongsTo(StoredStatus.self, key: "reblog")
static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")])) static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
.filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue) .filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue)
@ -63,23 +74,11 @@ extension StoredStatus {
through: descendantJoins, through: descendantJoins,
using: StatusContextJoin.status) using: StatusContextJoin.status)
var account: QueryInterfaceRequest<Account> { var ancestors: AnyFetchRequest<StatusResult> {
request(for: Self.account)
}
var reblogAccount: QueryInterfaceRequest<Account> {
request(for: Self.reblogAccount)
}
var reblog: QueryInterfaceRequest<StoredStatus> {
request(for: Self.reblog)
}
var ancestors: QueryInterfaceRequest<StatusResult> {
request(for: Self.ancestors).statusResultRequest request(for: Self.ancestors).statusResultRequest
} }
var descendants: QueryInterfaceRequest<StatusResult> { var descendants: AnyFetchRequest<StatusResult> {
request(for: Self.descendants).statusResultRequest request(for: Self.descendants).statusResultRequest
} }

View file

@ -4,12 +4,45 @@ import Foundation
import GRDB import GRDB
import Mastodon import Mastodon
extension Account: FetchableRecord, PersistableRecord { extension Account {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder { func save(_ db: Database) throws {
MastodonDecoder() if let moved = moved {
try StoredAccount(account: moved).save(db)
} }
public static func databaseJSONEncoder(for column: String) -> JSONEncoder { try StoredAccount(account: self).save(db)
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)
} }
} }

View file

@ -19,11 +19,11 @@ extension Status {
convenience init(statusResult: StatusResult) { convenience init(statusResult: StatusResult) {
var reblog: Status? var reblog: Status?
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount { if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccountResult {
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil) 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?) { convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {

View file

@ -41,7 +41,7 @@ extension Timeline {
using: TimelineStatusJoin.status) using: TimelineStatusJoin.status)
.order(Column("createdAt").desc) .order(Column("createdAt").desc)
var statuses: QueryInterfaceRequest<StatusResult> { var statuses: AnyFetchRequest<StatusResult> {
request(for: Self.statuses).statusResultRequest request(for: Self.statuses).statusResultRequest
} }
} }

View file

@ -2,7 +2,7 @@
import Foundation import Foundation
public struct Account: Codable, Hashable { public final class Account: Codable, Identifiable {
public struct Field: Codable, Hashable { public struct Field: Codable, Hashable {
public let name: String public let name: String
public let value: HTML public let value: HTML
@ -28,4 +28,95 @@ public struct Account: Codable, Hashable {
public let emojis: [Emoji] public let emojis: [Emoji]
@DecodableDefault.False public private(set) var bot: Bool @DecodableDefault.False public private(set) var bot: Bool
@DecodableDefault.False public private(set) var discoverable: 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)
}
} }

View file

@ -43,7 +43,6 @@ public final class Status: Codable, Identifiable {
@DecodableDefault.False public private(set) var bookmarked: Bool @DecodableDefault.False public private(set) var bookmarked: Bool
public let pinned: Bool? public let pinned: Bool?
// Xcode-generated memberwise initializer
public init( public init(
id: String, id: String,
uri: String, uri: String,