mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-24 09:10:59 +00:00
Modularize database code
This commit is contained in:
parent
280ec03ee9
commit
06b84a0aa7
28 changed files with 526 additions and 423 deletions
5
DB/.gitignore
vendored
Normal file
5
DB/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
28
DB/Package.swift
Normal file
28
DB/Package.swift
Normal 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"])
|
||||
]
|
||||
)
|
|
@ -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
|
18
DB/Sources/DB/Content/StatusContextJoin.swift
Normal file
18
DB/Sources/DB/Content/StatusContextJoin.swift
Normal 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")]))
|
||||
}
|
21
DB/Sources/DB/Content/StatusResult.swift
Normal file
21
DB/Sources/DB/Content/StatusResult.swift
Normal 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)
|
||||
}
|
||||
}
|
117
DB/Sources/DB/Content/StoredStatus.swift
Normal file
117
DB/Sources/DB/Content/StoredStatus.swift
Normal 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
|
||||
}
|
||||
}
|
11
DB/Sources/DB/Content/TimelineStatusJoin.swift
Normal file
11
DB/Sources/DB/Content/TimelineStatusJoin.swift
Normal 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)
|
||||
}
|
71
DB/Sources/DB/Entities/Identity.swift
Normal file
71
DB/Sources/DB/Entities/Identity.swift
Normal 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
|
||||
}
|
||||
}
|
18
DB/Sources/DB/Entities/IdentityFixture.swift
Normal file
18
DB/Sources/DB/Entities/IdentityFixture.swift
Normal 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
|
||||
}
|
||||
}
|
15
DB/Sources/DB/Extensions/Account+Extensions.swift
Normal file
15
DB/Sources/DB/Extensions/Account+Extensions.swift
Normal 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()
|
||||
}
|
||||
}
|
|
@ -1,9 +1,4 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Justin Mazzocchi on 9/2/20.
|
||||
//
|
||||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
15
DB/Sources/DB/Extensions/Filter+Extensions.swift
Normal file
15
DB/Sources/DB/Extensions/Filter+Extensions.swift
Normal 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()
|
||||
}
|
||||
}
|
22
DB/Sources/DB/Extensions/Identity+Internal.swift
Normal file
22
DB/Sources/DB/Extensions/Identity+Internal.swift
Normal 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 {}
|
61
DB/Sources/DB/Extensions/Status+ Extensions.swift
Normal file
61
DB/Sources/DB/Extensions/Status+ Extensions.swift
Normal 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)
|
||||
}
|
||||
}
|
47
DB/Sources/DB/Extensions/Timeline+Extensions.swift
Normal file
47
DB/Sources/DB/Extensions/Timeline+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 {}
|
12
DB/Sources/DB/Identity/IdentityResult.swift
Normal file
12
DB/Sources/DB/Identity/IdentityResult.swift
Normal 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
|
||||
}
|
28
DB/Sources/DB/Identity/StoredIdentity.swift
Normal file
28
DB/Sources/DB/Identity/StoredIdentity.swift
Normal 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)
|
||||
}
|
||||
}
|
10
DB/Tests/DBTests/DBTests.swift
Normal file
10
DB/Tests/DBTests/DBTests.swift
Normal 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.
|
||||
}
|
||||
}
|
|
@ -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 */,
|
||||
|
|
|
@ -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")]),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import DB
|
||||
import Foundation
|
||||
import Mastodon
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import DB
|
||||
import Mastodon
|
||||
|
||||
public struct StatusService {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import DB
|
||||
import Foundation
|
||||
import HTTP
|
||||
import ServiceLayer
|
||||
|
|
Loading…
Reference in a new issue