Modularize database code

This commit is contained in:
Justin Mazzocchi 2020-09-02 20:28:34 -07:00
parent 280ec03ee9
commit 06b84a0aa7
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
28 changed files with 526 additions and 423 deletions

5
DB/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

28
DB/Package.swift Normal file
View file

@ -0,0 +1,28 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "DB",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "DB",
targets: ["DB"])
],
dependencies: [
.package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", .upToNextMajor(from: "5.0.0-beta.10")),
.package(path: "Mastodon")
],
targets: [
.target(
name: "DB",
dependencies: ["GRDB", "Mastodon"]),
.testTarget(
name: "DBTests",
dependencies: ["DB"])
]
)

View file

@ -5,12 +5,11 @@ import Combine
import GRDB
import Mastodon
// swiftlint:disable file_length
struct ContentDatabase {
public struct ContentDatabase {
private let databaseQueue: DatabaseQueue
init(identityID: UUID, environment: AppEnvironment) throws {
if environment.inMemoryContent {
public init(identityID: UUID, inMemory: Bool) throws {
if inMemory {
databaseQueue = DatabaseQueue()
} else {
databaseQueue = try DatabaseQueue(path: try Self.fileURL(identityID: identityID).path)
@ -20,7 +19,7 @@ struct ContentDatabase {
}
}
extension ContentDatabase {
public extension ContentDatabase {
static func delete(forIdentityID identityID: UUID) throws {
try FileManager.default.removeItem(at: try fileURL(identityID: identityID))
}
@ -287,274 +286,3 @@ private extension ContentDatabase {
}
// swiftlint:enable function_body_length
}
extension Account: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}
private struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
let timelineId: String
let statusId: String
static let status = belongsTo(StoredStatus.self)
}
extension Timeline: FetchableRecord, PersistableRecord {
enum Columns: String, ColumnExpression {
case id, listTitle
}
public init(row: Row) {
switch (row[Columns.id] as String, row[Columns.listTitle] as String?) {
case (Timeline.home.id, _):
self = .home
case (Timeline.local.id, _):
self = .local
case (Timeline.federated.id, _):
self = .federated
case (let id, .some(let title)):
self = .list(MastodonList(id: id, title: title))
default:
self = .tag(row[Columns.id])
}
}
public func encode(to container: inout PersistenceContainer) {
container[Columns.id] = id
if case let .list(list) = self {
container[Columns.listTitle] = list.title
}
}
}
private extension Timeline {
static let statusJoins = hasMany(TimelineStatusJoin.self)
static let statuses = hasMany(
StoredStatus.self,
through: statusJoins,
using: TimelineStatusJoin.status)
.order(Column("createdAt").desc)
var statuses: QueryInterfaceRequest<StatusResult> {
request(for: Self.statuses).statusResultRequest
}
}
private struct StatusContextJoin: Codable, FetchableRecord, PersistableRecord {
enum Section: String, Codable {
case ancestors
case descendants
}
let parentId: String
let statusId: String
let section: Section
let index: Int
static let status = belongsTo(StoredStatus.self, using: ForeignKey([Column("statusId")]))
}
private extension StoredStatus {
static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
.filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue)
.order(Column("index"))
static let descendantJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
.filter(Column("section") == StatusContextJoin.Section.descendants.rawValue)
.order(Column("index"))
static let ancestors = hasMany(StoredStatus.self,
through: ancestorJoins,
using: StatusContextJoin.status)
static let descendants = hasMany(StoredStatus.self,
through: descendantJoins,
using: StatusContextJoin.status)
var ancestors: QueryInterfaceRequest<StatusResult> {
request(for: Self.ancestors).statusResultRequest
}
var descendants: QueryInterfaceRequest<StatusResult> {
request(for: Self.descendants).statusResultRequest
}
}
private extension QueryInterfaceRequest where RowDecoder == StoredStatus {
var statusResultRequest: QueryInterfaceRequest<StatusResult> {
including(required: StoredStatus.account)
.including(optional: StoredStatus.reblogAccount)
.including(optional: StoredStatus.reblog)
.asRequest(of: StatusResult.self)
}
}
extension Filter: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}
struct StoredStatus: Codable, Hashable {
let id: String
let uri: String
let createdAt: Date
let accountId: String
let content: HTML
let visibility: Status.Visibility
let sensitive: Bool
let spoilerText: String
let mediaAttachments: [Attachment]
let mentions: [Mention]
let tags: [Tag]
let emojis: [Emoji]
let reblogsCount: Int
let favouritesCount: Int
let repliesCount: Int
let application: Application?
let url: URL?
let inReplyToId: String?
let inReplyToAccountId: String?
let reblogId: String?
let poll: Poll?
let card: Card?
let language: String?
let text: String?
let favourited: Bool
let reblogged: Bool
let muted: Bool
let bookmarked: Bool
let pinned: Bool?
}
private 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 reblog = belongsTo(StoredStatus.self, key: "reblog")
var account: QueryInterfaceRequest<Account> {
request(for: Self.account)
}
var reblogAccount: QueryInterfaceRequest<Account> {
request(for: Self.reblogAccount)
}
var reblog: QueryInterfaceRequest<StoredStatus> {
request(for: Self.reblog)
}
init(status: Status) {
id = status.id
uri = status.uri
createdAt = status.createdAt
accountId = status.account.id
content = status.content
visibility = status.visibility
sensitive = status.sensitive
spoilerText = status.spoilerText
mediaAttachments = status.mediaAttachments
mentions = status.mentions
tags = status.tags
emojis = status.emojis
reblogsCount = status.reblogsCount
favouritesCount = status.favouritesCount
repliesCount = status.repliesCount
application = status.application
url = status.url
inReplyToId = status.inReplyToId
inReplyToAccountId = status.inReplyToAccountId
reblogId = status.reblog?.id
poll = status.poll
card = status.card
language = status.language
text = status.text
favourited = status.favourited
reblogged = status.reblogged
muted = status.muted
bookmarked = status.bookmarked
pinned = status.pinned
}
}
extension StoredStatus: FetchableRecord, PersistableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}
struct StatusResult: Codable, Hashable, FetchableRecord {
let account: Account
let status: StoredStatus
let reblogAccount: Account?
let reblog: StoredStatus?
}
private extension Status {
func save(_ db: Database) throws {
try account.save(db)
if let reblog = reblog {
try reblog.account.save(db)
try StoredStatus(status: reblog).save(db)
}
try StoredStatus(status: self).save(db)
}
convenience init(statusResult: StatusResult) {
var reblog: Status?
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount {
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil)
}
self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog)
}
convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {
self.init(
id: storedStatus.id,
uri: storedStatus.uri,
createdAt: storedStatus.createdAt,
account: account,
content: storedStatus.content,
visibility: storedStatus.visibility,
sensitive: storedStatus.sensitive,
spoilerText: storedStatus.spoilerText,
mediaAttachments: storedStatus.mediaAttachments,
mentions: storedStatus.mentions,
tags: storedStatus.tags,
emojis: storedStatus.emojis,
reblogsCount: storedStatus.reblogsCount,
favouritesCount: storedStatus.favouritesCount,
repliesCount: storedStatus.repliesCount,
application: storedStatus.application,
url: storedStatus.url,
inReplyToId: storedStatus.inReplyToId,
inReplyToAccountId: storedStatus.inReplyToAccountId,
reblog: reblog,
poll: storedStatus.poll,
card: storedStatus.card,
language: storedStatus.language,
text: storedStatus.text,
favourited: storedStatus.favourited,
reblogged: storedStatus.reblogged,
muted: storedStatus.muted,
bookmarked: storedStatus.bookmarked,
pinned: storedStatus.pinned)
}
}
// swiftlint:enable file_length

View file

@ -0,0 +1,18 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
struct StatusContextJoin: Codable, FetchableRecord, PersistableRecord {
enum Section: String, Codable {
case ancestors
case descendants
}
let parentId: String
let statusId: String
let section: Section
let index: Int
static let status = belongsTo(StoredStatus.self, using: ForeignKey([Column("statusId")]))
}

View file

@ -0,0 +1,21 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
struct StatusResult: Codable, Hashable, FetchableRecord {
let account: Account
let status: StoredStatus
let reblogAccount: Account?
let reblog: StoredStatus?
}
extension QueryInterfaceRequest where RowDecoder == StoredStatus {
var statusResultRequest: QueryInterfaceRequest<StatusResult> {
including(required: StoredStatus.account)
.including(optional: StoredStatus.reblogAccount)
.including(optional: StoredStatus.reblog)
.asRequest(of: StatusResult.self)
}
}

View file

@ -0,0 +1,117 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
struct StoredStatus: Codable, Hashable {
let id: String
let uri: String
let createdAt: Date
let accountId: String
let content: HTML
let visibility: Status.Visibility
let sensitive: Bool
let spoilerText: String
let mediaAttachments: [Attachment]
let mentions: [Mention]
let tags: [Tag]
let emojis: [Emoji]
let reblogsCount: Int
let favouritesCount: Int
let repliesCount: Int
let application: Application?
let url: URL?
let inReplyToId: String?
let inReplyToAccountId: String?
let reblogId: String?
let poll: Poll?
let card: Card?
let language: String?
let text: String?
let favourited: Bool
let reblogged: Bool
let muted: Bool
let bookmarked: Bool
let pinned: Bool?
}
extension StoredStatus: FetchableRecord, PersistableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}
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 reblog = belongsTo(StoredStatus.self, key: "reblog")
static let ancestorJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
.filter(Column("section") == StatusContextJoin.Section.ancestors.rawValue)
.order(Column("index"))
static let descendantJoins = hasMany(StatusContextJoin.self, using: ForeignKey([Column("parentID")]))
.filter(Column("section") == StatusContextJoin.Section.descendants.rawValue)
.order(Column("index"))
static let ancestors = hasMany(StoredStatus.self,
through: ancestorJoins,
using: StatusContextJoin.status)
static let descendants = hasMany(StoredStatus.self,
through: descendantJoins,
using: StatusContextJoin.status)
var account: QueryInterfaceRequest<Account> {
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
}
var descendants: QueryInterfaceRequest<StatusResult> {
request(for: Self.descendants).statusResultRequest
}
init(status: Status) {
id = status.id
uri = status.uri
createdAt = status.createdAt
accountId = status.account.id
content = status.content
visibility = status.visibility
sensitive = status.sensitive
spoilerText = status.spoilerText
mediaAttachments = status.mediaAttachments
mentions = status.mentions
tags = status.tags
emojis = status.emojis
reblogsCount = status.reblogsCount
favouritesCount = status.favouritesCount
repliesCount = status.repliesCount
application = status.application
url = status.url
inReplyToId = status.inReplyToId
inReplyToAccountId = status.inReplyToAccountId
reblogId = status.reblog?.id
poll = status.poll
card = status.card
language = status.language
text = status.text
favourited = status.favourited
reblogged = status.reblogged
muted = status.muted
bookmarked = status.bookmarked
pinned = status.pinned
}
}

View file

@ -0,0 +1,11 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
let timelineId: String
let statusId: String
static let status = belongsTo(StoredStatus.self)
}

View file

@ -0,0 +1,71 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
public struct Identity: Codable, Hashable, Identifiable {
public let id: UUID
public let url: URL
public let lastUsedAt: Date
public let preferences: Identity.Preferences
public let instance: Identity.Instance?
public let account: Identity.Account?
public let lastRegisteredDeviceToken: String?
public let pushSubscriptionAlerts: PushSubscription.Alerts
}
public extension Identity {
struct Instance: Codable, Hashable {
public let uri: String
public let streamingAPI: URL
public let title: String
public let thumbnail: URL?
}
struct Account: Codable, Hashable {
public let id: String
public let identityID: UUID
public let username: String
public let displayName: String
public let url: URL
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public let emojis: [Emoji]
}
struct Preferences: Codable, Hashable {
@DecodableDefault.True public var useServerPostingReadingPreferences
@DecodableDefault.StatusVisibilityPublic public var postingDefaultVisibility: Status.Visibility
@DecodableDefault.False public var postingDefaultSensitive
public var postingDefaultLanguage: String?
@DecodableDefault.ExpandMediaDefault public var readingExpandMedia: Mastodon.Preferences.ExpandMedia
@DecodableDefault.False public var readingExpandSpoilers
}
var handle: String {
if let account = account, let host = account.url.host {
return account.url.lastPathComponent + "@" + host
}
return instance?.title ?? url.host ?? url.absoluteString
}
var image: URL? { account?.avatar ?? instance?.thumbnail }
}
public extension Identity.Preferences {
func updated(from serverPreferences: Preferences) -> Self {
var mutable = self
if useServerPostingReadingPreferences {
mutable.postingDefaultVisibility = serverPreferences.postingDefaultVisibility
mutable.postingDefaultSensitive = serverPreferences.postingDefaultSensitive
mutable.readingExpandMedia = serverPreferences.readingExpandMedia
mutable.readingExpandSpoilers = serverPreferences.readingExpandSpoilers
}
return mutable
}
}

View file

@ -0,0 +1,18 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
public struct IdentityFixture {
public let id: UUID
public let instanceURL: URL
public let instance: Instance?
public let account: Account?
public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) {
self.id = id
self.instanceURL = instanceURL
self.instance = instance
self.account = account
}
}

View file

@ -0,0 +1,15 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
extension Account: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}

View file

@ -1,9 +1,4 @@
//
// File.swift
//
//
// Created by Justin Mazzocchi on 9/2/20.
//
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation

View file

@ -0,0 +1,15 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
extension Filter: FetchableRecord, PersistableRecord {
public static func databaseJSONDecoder(for column: String) -> JSONDecoder {
APIDecoder()
}
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
APIEncoder()
}
}

View file

@ -0,0 +1,22 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
extension Identity {
init(result: IdentityResult) {
self.init(
id: result.identity.id,
url: result.identity.url,
lastUsedAt: result.identity.lastUsedAt,
preferences: result.identity.preferences,
instance: result.instance,
account: result.account,
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
}
}
extension Identity.Instance: FetchableRecord, PersistableRecord {}
extension Identity.Account: FetchableRecord, PersistableRecord {}

View file

@ -0,0 +1,61 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
extension Status {
func save(_ db: Database) throws {
try account.save(db)
if let reblog = reblog {
try reblog.account.save(db)
try StoredStatus(status: reblog).save(db)
}
try StoredStatus(status: self).save(db)
}
convenience init(statusResult: StatusResult) {
var reblog: Status?
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount {
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil)
}
self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog)
}
convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {
self.init(
id: storedStatus.id,
uri: storedStatus.uri,
createdAt: storedStatus.createdAt,
account: account,
content: storedStatus.content,
visibility: storedStatus.visibility,
sensitive: storedStatus.sensitive,
spoilerText: storedStatus.spoilerText,
mediaAttachments: storedStatus.mediaAttachments,
mentions: storedStatus.mentions,
tags: storedStatus.tags,
emojis: storedStatus.emojis,
reblogsCount: storedStatus.reblogsCount,
favouritesCount: storedStatus.favouritesCount,
repliesCount: storedStatus.repliesCount,
application: storedStatus.application,
url: storedStatus.url,
inReplyToId: storedStatus.inReplyToId,
inReplyToAccountId: storedStatus.inReplyToAccountId,
reblog: reblog,
poll: storedStatus.poll,
card: storedStatus.card,
language: storedStatus.language,
text: storedStatus.text,
favourited: storedStatus.favourited,
reblogged: storedStatus.reblogged,
muted: storedStatus.muted,
bookmarked: storedStatus.bookmarked,
pinned: storedStatus.pinned)
}
}

View file

@ -0,0 +1,47 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
extension Timeline: FetchableRecord, PersistableRecord {
enum Columns: String, ColumnExpression {
case id, listTitle
}
public init(row: Row) {
switch (row[Columns.id] as String, row[Columns.listTitle] as String?) {
case (Timeline.home.id, _):
self = .home
case (Timeline.local.id, _):
self = .local
case (Timeline.federated.id, _):
self = .federated
case (let id, .some(let title)):
self = .list(MastodonList(id: id, title: title))
default:
self = .tag(row[Columns.id])
}
}
public func encode(to container: inout PersistenceContainer) {
container[Columns.id] = id
if case let .list(list) = self {
container[Columns.listTitle] = list.title
}
}
}
extension Timeline {
static let statusJoins = hasMany(TimelineStatusJoin.self)
static let statuses = hasMany(
StoredStatus.self,
through: statusJoins,
using: TimelineStatusJoin.status)
.order(Column("createdAt").desc)
var statuses: QueryInterfaceRequest<StatusResult> {
request(for: Self.statuses).statusResultRequest
}
}

View file

@ -5,15 +5,15 @@ import Combine
import GRDB
import Mastodon
enum IdentityDatabaseError: Error {
public enum IdentityDatabaseError: Error {
case identityNotFound
}
struct IdentityDatabase {
public struct IdentityDatabase {
private let databaseQueue: DatabaseQueue
init(environment: AppEnvironment) throws {
if environment.inMemoryContent {
public init(inMemory: Bool, fixture: IdentityFixture?) throws {
if inMemory {
databaseQueue = DatabaseQueue()
} else {
let databaseURL = try FileManager.default.databaseDirectoryURL().appendingPathComponent("Identities.sqlite")
@ -23,13 +23,13 @@ struct IdentityDatabase {
try Self.migrate(databaseQueue)
if let fixture = environment.identityFixture {
if let fixture = fixture {
try populate(fixture: fixture)
}
}
}
extension IdentityDatabase {
public extension IdentityDatabase {
func createIdentity(id: UUID, url: URL) -> AnyPublisher<Never, Error> {
databaseQueue.writePublisher(
updates: StoredIdentity(
@ -235,7 +235,7 @@ private extension IdentityDatabase {
try migrator.migrate(writer)
}
func populate(fixture: AppEnvironment.IdentityFixture) throws {
func populate(fixture: IdentityFixture) throws {
_ = createIdentity(id: fixture.id, url: fixture.instanceURL)
.receive(on: ImmediateScheduler.shared)
.sink { _ in } receiveValue: { _ in }
@ -253,51 +253,3 @@ private extension IdentityDatabase {
}
}
}
private struct StoredIdentity: Codable, Hashable, FetchableRecord, PersistableRecord {
let id: UUID
let url: URL
let lastUsedAt: Date
let preferences: Identity.Preferences
let instanceURI: String?
let lastRegisteredDeviceToken: String?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
extension StoredIdentity {
static let instance = belongsTo(Identity.Instance.self, key: "instance")
static let account = hasOne(Identity.Account.self, key: "account")
var instance: QueryInterfaceRequest<Identity.Instance> {
request(for: Self.instance)
}
var account: QueryInterfaceRequest<Identity.Account> {
request(for: Self.account)
}
}
private struct IdentityResult: Codable, Hashable, FetchableRecord {
let identity: StoredIdentity
let instance: Identity.Instance?
let account: Identity.Account?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
private extension Identity {
init(result: IdentityResult) {
self.init(
id: result.identity.id,
url: result.identity.url,
lastUsedAt: result.identity.lastUsedAt,
preferences: result.identity.preferences,
instance: result.instance,
account: result.account,
lastRegisteredDeviceToken: result.identity.lastRegisteredDeviceToken,
pushSubscriptionAlerts: result.pushSubscriptionAlerts)
}
}
extension Identity.Instance: FetchableRecord, PersistableRecord {}
extension Identity.Account: FetchableRecord, PersistableRecord {}

View file

@ -0,0 +1,12 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
struct IdentityResult: Codable, Hashable, FetchableRecord {
let identity: StoredIdentity
let instance: Identity.Instance?
let account: Identity.Account?
let pushSubscriptionAlerts: PushSubscription.Alerts
}

View file

@ -0,0 +1,28 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import GRDB
import Mastodon
struct StoredIdentity: Codable, Hashable, FetchableRecord, PersistableRecord {
let id: UUID
let url: URL
let lastUsedAt: Date
let preferences: Identity.Preferences
let instanceURI: String?
let lastRegisteredDeviceToken: String?
let pushSubscriptionAlerts: PushSubscription.Alerts
}
extension StoredIdentity {
static let instance = belongsTo(Identity.Instance.self, key: "instance")
static let account = hasOne(Identity.Account.self, key: "account")
var instance: QueryInterfaceRequest<Identity.Instance> {
request(for: Self.instance)
}
var account: QueryInterfaceRequest<Identity.Account> {
request(for: Self.account)
}
}

View file

@ -0,0 +1,10 @@
import XCTest
@testable import DB
final class DBTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

View file

@ -85,6 +85,7 @@
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = "<group>"; };
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
@ -176,6 +177,7 @@
isa = PBXGroup;
children = (
D0C7D45224F76169001EBDBB /* Assets.xcassets */,
D085C3BB25008DEC008A6C5E /* DB */,
D0C7D46824F76169001EBDBB /* Extensions */,
D0666A7924C7745A00F3F04B /* Frameworks */,
D0BFDAF524FC7C5300C86618 /* HTTP */,

View file

@ -18,13 +18,13 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
.package(name: "GRDB", url: "https://github.com/groue/GRDB.swift.git", .upToNextMajor(from: "5.0.0-beta.10")),
.package(path: "DB"),
.package(path: "Mastodon")
],
targets: [
.target(
name: "ServiceLayer",
dependencies: ["GRDB", "Mastodon"]),
dependencies: ["DB"]),
.target(
name: "ServiceLayerMocks",
dependencies: ["ServiceLayer", .product(name: "MastodonStubs", package: "Mastodon")]),

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
import DB
import Foundation
import Combine
import Mastodon
@ -11,7 +12,8 @@ public struct AllIdentitiesService {
private let environment: AppEnvironment
public init(environment: AppEnvironment) throws {
self.identityDatabase = try IdentityDatabase(environment: environment)
self.identityDatabase = try IdentityDatabase(inMemory: environment.inMemoryContent,
fixture: environment.identityFixture)
self.environment = environment
mostRecentlyUsedIdentityID = identityDatabase.mostRecentlyUsedIdentityIDObservation()

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
import DB
import Foundation
import HTTP
import Mastodon
@ -32,20 +33,6 @@ public struct AppEnvironment {
}
public extension AppEnvironment {
struct IdentityFixture {
public let id: UUID
public let instanceURL: URL
public let instance: Instance?
public let account: Account?
public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) {
self.id = id
self.instanceURL = instanceURL
self.instance = instance
self.account = account
}
}
static func live(userNotificationCenter: UNUserNotificationCenter) -> Self {
Self(
session: Session(configuration: .default),

View file

@ -1,71 +1,5 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
import DB
public struct Identity: Codable, Hashable, Identifiable {
public let id: UUID
public let url: URL
public let lastUsedAt: Date
public let preferences: Identity.Preferences
public let instance: Identity.Instance?
public let account: Identity.Account?
public let lastRegisteredDeviceToken: String?
public let pushSubscriptionAlerts: PushSubscription.Alerts
}
public extension Identity {
struct Instance: Codable, Hashable {
public let uri: String
public let streamingAPI: URL
public let title: String
public let thumbnail: URL?
}
struct Account: Codable, Hashable {
public let id: String
public let identityID: UUID
public let username: String
public let displayName: String
public let url: URL
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public let emojis: [Emoji]
}
struct Preferences: Codable, Hashable {
@DecodableDefault.True public var useServerPostingReadingPreferences
@DecodableDefault.StatusVisibilityPublic public var postingDefaultVisibility: Status.Visibility
@DecodableDefault.False public var postingDefaultSensitive
public var postingDefaultLanguage: String?
@DecodableDefault.ExpandMediaDefault public var readingExpandMedia: Mastodon.Preferences.ExpandMedia
@DecodableDefault.False public var readingExpandSpoilers
}
var handle: String {
if let account = account, let host = account.url.host {
return account.url.lastPathComponent + "@" + host
}
return instance?.title ?? url.host ?? url.absoluteString
}
var image: URL? { account?.avatar ?? instance?.thumbnail }
}
public extension Identity.Preferences {
func updated(from serverPreferences: Preferences) -> Self {
var mutable = self
if useServerPostingReadingPreferences {
mutable.postingDefaultVisibility = serverPreferences.postingDefaultVisibility
mutable.postingDefaultSensitive = serverPreferences.postingDefaultSensitive
mutable.readingExpandMedia = serverPreferences.readingExpandMedia
mutable.readingExpandSpoilers = serverPreferences.readingExpandSpoilers
}
return mutable
}
}
public typealias Identity = DB.Identity

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
import DB
import Foundation
import Combine
import Mastodon
@ -39,7 +40,7 @@ public class IdentityService {
networkClient.instanceURL = identity.url
networkClient.accessToken = try? secretsService.item(.accessToken)
contentDatabase = try ContentDatabase(identityID: identityID, environment: environment)
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
observation.catch { [weak self] error -> Empty<Identity, Never> in
self?.observationErrorsInput.send(error)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
import DB
import Foundation
import Mastodon

View file

@ -2,6 +2,7 @@
import Foundation
import Combine
import DB
import Mastodon
public struct StatusService {

View file

@ -1,5 +1,6 @@
// Copyright © 2020 Metabolist. All rights reserved.
import DB
import Foundation
import HTTP
import ServiceLayer