Convert API objects to CoreData models and save them

This commit is contained in:
Shadowfacts 2020-04-11 22:23:31 -04:00
parent 7deb4fc5b4
commit 102fe6ed91
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
5 changed files with 161 additions and 29 deletions

View File

@ -12,29 +12,29 @@ import CoreData
import Pachyderm import Pachyderm
@objc(AccountMO) @objc(AccountMO)
public class AccountMO: NSManagedObject { public final class AccountMO: NSManagedObject {
@nonobjc public class func fetchRequest() -> NSFetchRequest<AccountMO> { @nonobjc public class func fetchRequest() -> NSFetchRequest<AccountMO> {
return NSFetchRequest<AccountMO>(entityName: "Account") return NSFetchRequest<AccountMO>(entityName: "Account")
} }
@NSManaged public var acct: String? @NSManaged public var acct: String
@NSManaged public var avatar: URL? @NSManaged public var avatar: URL
@NSManaged public var bot: Bool @NSManaged public var bot: Bool
@NSManaged public var createdAt: Date? @NSManaged public var createdAt: Date
@NSManaged public var displayName: String? @NSManaged public var displayName: String
@NSManaged public var emojisData: Data? @NSManaged private var emojisData: Data?
@NSManaged public var fieldsData: Data? @NSManaged private var fieldsData: Data?
@NSManaged public var followersCount: Int64 @NSManaged public var followersCount: Int
@NSManaged public var followingCount: Int64 @NSManaged public var followingCount: Int
@NSManaged public var header: URL? @NSManaged public var header: URL
@NSManaged public var id: String? @NSManaged public var id: String
@NSManaged public var locked: Bool @NSManaged public var locked: Bool
@NSManaged public var moved: Bool @NSManaged public var moved: Bool
@NSManaged public var note: String? @NSManaged public var note: String
@NSManaged public var statusesCount: Int64 @NSManaged public var statusesCount: Int
@NSManaged public var url: URL? @NSManaged public var url: URL
@NSManaged public var username: String? @NSManaged public var username: String
@NSManaged public var movedTo: AccountMO? @NSManaged public var movedTo: AccountMO?
@LazilyDecoding(arrayFrom: \AccountMO.emojisData) @LazilyDecoding(arrayFrom: \AccountMO.emojisData)
@ -44,3 +44,40 @@ public class AccountMO: NSManagedObject {
var fields: [Account.Field] var fields: [Account.Field]
} }
extension AccountMO {
convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
self.init(context: context)
self.updateFrom(apiAccount: account, container: container)
}
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {
guard let context = managedObjectContext else {
// we've been deleted, don't bother updating
return
}
self.acct = account.acct
self.avatar = account.avatar
self.bot = account.bot ?? false
self.createdAt = account.createdAt
self.displayName = account.displayName
self.emojis = account.emojis
self.fields = account.fields ?? []
self.followersCount = account.followersCount
self.followingCount = account.followingCount
self.header = account.header
self.id = account.id
self.locked = account.locked
self.moved = account.moved ?? false
self.note = account.note
self.statusesCount = account.statusesCount
self.url = account.url
self.username = account.username
if let movedTo = account.movedTo {
self.movedTo = container.account(for: movedTo.id, in: context) ?? AccountMO(apiAccount: movedTo, container: container, context: context)
} else {
self.movedTo = nil
}
}
}

View File

@ -8,9 +8,12 @@
import Foundation import Foundation
import CoreData import CoreData
import Pachyderm
class MastodonCachePersistentStore: NSPersistentContainer { class MastodonCachePersistentStore: NSPersistentContainer {
private(set) lazy var backgroundContext = newBackgroundContext()
init(for controller: MastodonController) { init(for controller: MastodonController) {
let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")! let url = Bundle.main.url(forResource: "Tusker", withExtension: "momd")!
let model = NSManagedObjectModel(contentsOf: url)! let model = NSManagedObjectModel(contentsOf: url)!
@ -22,26 +25,54 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
} }
func status(for id: String) -> StatusMO? { func status(for id: String, in context: NSManagedObjectContext? = nil) -> StatusMO? {
let context = context ?? viewContext
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest() let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
request.predicate = NSPredicate(format: "id = %@", id) request.predicate = NSPredicate(format: "id = %@", id)
request.fetchLimit = 1 request.fetchLimit = 1
if let result = try? viewContext.fetch(request), let status = result.first { if let result = try? context.fetch(request), let status = result.first {
return status return status
} else { } else {
return nil return nil
} }
} }
func account(for id: String) -> AccountMO? { func addOrUpdate(status: Status, save: Bool = true) {
backgroundContext.perform {
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
statusMO.updateFrom(apiStatus: status, container: self)
} else {
_ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
}
}
func account(for id: String, in context: NSManagedObjectContext? = nil) -> AccountMO? {
let context = context ?? viewContext
let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest() let request: NSFetchRequest<AccountMO> = AccountMO.fetchRequest()
request.predicate = NSPredicate(format: "id = %@", id) request.predicate = NSPredicate(format: "id = %@", id)
request.fetchLimit = 1 request.fetchLimit = 1
if let result = try? viewContext.fetch(request), let account = result.first { if let result = try? context.fetch(request), let account = result.first {
return account return account
} else { } else {
return nil return nil
} }
} }
func addOrUpdate(account: Account, save: Bool = true) {
backgroundContext.perform {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self)
} else {
_ = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
}
if save, self.backgroundContext.hasChanges {
try! self.backgroundContext.save()
}
}
}
} }

View File

@ -19,7 +19,7 @@ public final class StatusMO: NSManagedObject {
} }
@NSManaged public var application: String? @NSManaged public var application: String?
@NSManaged public var attachmentsData: Data? @NSManaged private var attachmentsData: Data?
@NSManaged public var bookmarked: Bool @NSManaged public var bookmarked: Bool
@NSManaged public var content: String @NSManaged public var content: String
@NSManaged public var createdAt: Date @NSManaged public var createdAt: Date
@ -28,25 +28,25 @@ public final class StatusMO: NSManagedObject {
@NSManaged public var favouritesCount: Int @NSManaged public var favouritesCount: Int
@NSManaged public var hashtagsData: Data? @NSManaged public var hashtagsData: Data?
@NSManaged public var id: String @NSManaged public var id: String
@NSManaged public var mentionsData: Data? @NSManaged public var inReplyToAccountID: String?
@NSManaged public var inReplyToID: String?
@NSManaged private var mentionsData: Data?
@NSManaged public var muted: Bool @NSManaged public var muted: Bool
@NSManaged public var pinned: Bool @NSManaged public var pinned: Bool
@NSManaged public var reblogged: Bool @NSManaged public var reblogged: Bool
@NSManaged public var reblogsCount: Int @NSManaged public var reblogsCount: Int
@NSManaged public var sensitive: Bool @NSManaged public var sensitive: Bool
@NSManaged public var uri: String @NSManaged public var uri: String // todo: are both uri and url necessary?
@NSManaged public var url: URL? @NSManaged public var url: URL?
@NSManaged public var visibility: String? @NSManaged private var visibilityString: String
@NSManaged public var account: AccountMO @NSManaged public var account: AccountMO
@NSManaged public var inReplyTo: StatusMO?
@NSManaged public var inReplyToAccount: AccountMO?
@NSManaged public var reblog: StatusMO? @NSManaged public var reblog: StatusMO?
@LazilyDecoding(arrayFrom: \StatusMO.attachmentsData) @LazilyDecoding(arrayFrom: \StatusMO.attachmentsData)
var attachments: [Attachment] var attachments: [Attachment]
@LazilyDecoding(arrayFrom: \StatusMO.emojisData) @LazilyDecoding(arrayFrom: \StatusMO.emojisData)
var emoji: [Emoji] var emojis: [Emoji]
@LazilyDecoding(arrayFrom: \StatusMO.hashtagsData) @LazilyDecoding(arrayFrom: \StatusMO.hashtagsData)
var hashtags: [Hashtag] var hashtags: [Hashtag]
@ -54,4 +54,54 @@ public final class StatusMO: NSManagedObject {
@LazilyDecoding(arrayFrom: \StatusMO.mentionsData) @LazilyDecoding(arrayFrom: \StatusMO.mentionsData)
var mentions: [Mention] var mentions: [Mention]
var visibility: Status.Visibility {
get {
Pachyderm.Status.Visibility(rawValue: visibilityString) ?? .public
}
set {
visibilityString = newValue.rawValue
}
}
}
extension StatusMO {
convenience init(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
self.init(context: context)
self.updateFrom(apiStatus: status, container: container)
}
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {
guard let context = managedObjectContext else {
// we have been deleted, don't bother updating
return
}
self.application = status.application?.name
self.attachments = status.attachments
self.bookmarked = status.bookmarked ?? false
self.content = status.content
self.createdAt = status.createdAt
self.emojis = status.emojis
self.favourited = status.favourited ?? false
self.favouritesCount = status.favouritesCount
self.hashtags = status.hashtags
self.inReplyToAccountID = status.inReplyToAccountID
self.inReplyToID = status.inReplyToID
self.id = status.id
self.mentions = status.mentions
self.muted = status.muted ?? false
self.pinned = status.pinned ?? false
self.reblogged = status.reblogged ?? false
self.reblogsCount = status.reblogsCount
self.sensitive = status.sensitive
self.uri = status.uri
self.visibility = status.visibility
self.account = container.account(for: status.account.id, in: context) ?? AccountMO(apiAccount: status.account, container: container, context: context)
if let reblog = status.reblog {
self.reblog = container.status(for: reblog.id, in: context) ?? StatusMO(apiStatus: reblog, container: container, context: context)
} else {
self.reblog = nil
}
}
} }

View File

@ -36,6 +36,8 @@
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hashtagsData" attributeType="Binary"/> <attribute name="hashtagsData" attributeType="Binary"/>
<attribute name="id" attributeType="String"/> <attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="mentionsData" attributeType="Binary"/> <attribute name="mentionsData" attributeType="Binary"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pinned" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="pinned" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -44,10 +46,8 @@
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="uri" attributeType="String"/> <attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/> <attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibility" attributeType="String"/> <attribute name="visibilityString" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/> <relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="inReplyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<relationship name="inReplyToAccount" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/> <relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<uniquenessConstraints> <uniquenessConstraints>
<uniquenessConstraint> <uniquenessConstraint>

View File

@ -59,10 +59,17 @@ class MastodonCache {
func add(status: Status) { func add(status: Status) {
set(status: status, for: status.id) set(status: status, for: status.id)
mastodonController?.persistentContainer.addOrUpdate(status: status)
} }
func addAll(statuses: [Status]) { func addAll(statuses: [Status]) {
statuses.forEach(add) statuses.forEach(add)
if let container = mastodonController?.persistentContainer {
statuses.forEach { container.addOrUpdate(status: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Accounts // MARK: - Accounts
@ -92,10 +99,17 @@ class MastodonCache {
func add(account: Account) { func add(account: Account) {
set(account: account, for: account.id) set(account: account, for: account.id)
mastodonController?.persistentContainer.addOrUpdate(account: account)
} }
func addAll(accounts: [Account]) { func addAll(accounts: [Account]) {
accounts.forEach(add) accounts.forEach(add)
if let container = mastodonController?.persistentContainer {
accounts.forEach { container.addOrUpdate(account: $0, save: false) }
container.backgroundContext.perform {
try! container.backgroundContext.save()
}
}
} }
// MARK: - Relationships // MARK: - Relationships