mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Improve context data modeling
This commit is contained in:
parent
bc59acd160
commit
6307381375
5 changed files with 135 additions and 124 deletions
|
@ -24,21 +24,52 @@ struct ContentDatabase {
|
|||
}
|
||||
|
||||
try Self.migrate(databaseQueue)
|
||||
try Self.createTemporaryTables(databaseQueue)
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentDatabase {
|
||||
func insert(statuses: [Status], collection: StatusCollection? = nil) -> AnyPublisher<Never, Error> {
|
||||
func insert(statuses: [Status], timeline: Timeline? = nil) -> AnyPublisher<Never, Error> {
|
||||
databaseQueue.writePublisher {
|
||||
try collection?.save($0)
|
||||
try timeline?.save($0)
|
||||
|
||||
for status in statuses {
|
||||
for component in status.storedComponents() {
|
||||
try component.save($0)
|
||||
}
|
||||
|
||||
try collection?.joinRecord(status: status).save($0)
|
||||
if let timeline = timeline {
|
||||
try TimelineStatusJoin(timelineId: timeline.id, statusId: status.id).save($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func insert(context: Context, parentID: String) -> AnyPublisher<Never, Error> {
|
||||
databaseQueue.writePublisher {
|
||||
for status in context.ancestors + context.descendants {
|
||||
for component in status.storedComponents() {
|
||||
try component.save($0)
|
||||
}
|
||||
}
|
||||
|
||||
for (section, statuses) in [(StatusContextJoin.Section.ancestors, context.ancestors),
|
||||
(StatusContextJoin.Section.descendants, context.descendants)] {
|
||||
for (index, status) in statuses.enumerated() {
|
||||
try StatusContextJoin(
|
||||
parentId: parentID,
|
||||
statusId: status.id,
|
||||
section: section,
|
||||
index: index)
|
||||
.save($0)
|
||||
}
|
||||
|
||||
try StatusContextJoin.filter(
|
||||
Column("parentId") == parentID
|
||||
&& Column("section") == section.rawValue
|
||||
&& Column("index") >= statuses.count)
|
||||
.deleteAll($0)
|
||||
}
|
||||
}
|
||||
.ignoreOutput()
|
||||
|
@ -93,11 +124,28 @@ extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusesObservation(collection: StatusCollection) -> AnyPublisher<[Status], Error> {
|
||||
ValueObservation.tracking(collection.fetch)
|
||||
func statusesObservation(timeline: Timeline) -> AnyPublisher<[[Status]], Error> {
|
||||
ValueObservation.tracking(timeline.statuses.fetchAll)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseQueue)
|
||||
.map { $0.map(Status.init(statusResult:)) }
|
||||
.map { [$0.map(Status.init(statusResult:))] }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func contextObservation(parentID: String) -> AnyPublisher<[[Status]], Error> {
|
||||
ValueObservation.tracking { db -> [[StatusResult]] in
|
||||
guard let parent = try StoredStatus.filter(Column("id") == parentID).statusResultRequest.fetchOne(db) else {
|
||||
return [[]]
|
||||
}
|
||||
|
||||
let ancestors = try parent.status.ancestors.fetchAll(db)
|
||||
let descendants = try parent.status.descendants.fetchAll(db)
|
||||
|
||||
return [ancestors, [parent], descendants]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseQueue)
|
||||
.map { $0.map { $0.map(Status.init(statusResult:)) } }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -209,6 +257,21 @@ private extension ContentDatabase {
|
|||
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
|
||||
try db.create(table: "statusContextJoin", ifNotExists: true) { t in
|
||||
t.column("parentId", .text)
|
||||
.indexed()
|
||||
.notNull()
|
||||
.references("storedStatus", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text)
|
||||
.indexed()
|
||||
.notNull()
|
||||
.references("storedStatus", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("section", .text).notNull()
|
||||
t.column("index", .integer).notNull()
|
||||
|
||||
t.primaryKey(["parentId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
|
||||
try db.create(table: "filter", ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
t.column("phrase", .text).notNull()
|
||||
|
@ -222,23 +285,6 @@ private extension ContentDatabase {
|
|||
try migrator.migrate(writer)
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
static func createTemporaryTables(_ writer: DatabaseWriter) throws {
|
||||
try writer.write { database in
|
||||
try database.create(table: "transientStatusCollection", temporary: true, ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
}
|
||||
|
||||
try database.create(table: "transientStatusCollectionElement", temporary: true, ifNotExists: true) { t in
|
||||
t.column("transientStatusCollectionId", .text)
|
||||
.notNull()
|
||||
.references("transientStatusCollection", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).notNull()
|
||||
|
||||
t.primaryKey(["transientStatusCollectionId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Account: FetchableRecord, PersistableRecord {
|
||||
|
@ -251,13 +297,6 @@ extension Account: FetchableRecord, PersistableRecord {
|
|||
}
|
||||
}
|
||||
|
||||
public protocol StatusCollection: FetchableRecord, PersistableRecord {
|
||||
var id: String { get }
|
||||
var fetch: (Database) throws -> [StatusResult] { get }
|
||||
|
||||
func joinRecord(status: Status) -> PersistableRecord
|
||||
}
|
||||
|
||||
private struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
||||
let timelineId: String
|
||||
let statusId: String
|
||||
|
@ -265,7 +304,7 @@ private struct TimelineStatusJoin: Codable, FetchableRecord, PersistableRecord {
|
|||
static let status = belongsTo(StoredStatus.self)
|
||||
}
|
||||
|
||||
extension Timeline: StatusCollection {
|
||||
extension Timeline: FetchableRecord, PersistableRecord {
|
||||
enum Columns: String, ColumnExpression {
|
||||
case id, listTitle
|
||||
}
|
||||
|
@ -292,34 +331,64 @@ extension Timeline: StatusCollection {
|
|||
container[Columns.listTitle] = list.title
|
||||
}
|
||||
}
|
||||
|
||||
public var fetch: (Database) throws -> [StatusResult] {
|
||||
statuses
|
||||
.including(required: StoredStatus.account)
|
||||
.including(optional: StoredStatus.reblogAccount)
|
||||
.including(optional: StoredStatus.reblog)
|
||||
.asRequest(of: StatusResult.self)
|
||||
.fetchAll
|
||||
}
|
||||
|
||||
public func joinRecord(status: Status) -> PersistableRecord {
|
||||
TimelineStatusJoin(timelineId: id, statusId: status.id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Timeline {
|
||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
||||
|
||||
static let statuses = hasMany(StoredStatus.self,
|
||||
static let statuses = hasMany(
|
||||
StoredStatus.self,
|
||||
through: statusJoins,
|
||||
using: TimelineStatusJoin.status).order(Column("createdAt").desc)
|
||||
using: TimelineStatusJoin.status)
|
||||
.order(Column("createdAt").desc)
|
||||
|
||||
var statusJoins: QueryInterfaceRequest<TimelineStatusJoin> {
|
||||
request(for: Self.statusJoins)
|
||||
var statuses: QueryInterfaceRequest<StatusResult> {
|
||||
request(for: Self.statuses).statusResultRequest
|
||||
}
|
||||
}
|
||||
|
||||
private struct StatusContextJoin: Codable, FetchableRecord, PersistableRecord {
|
||||
enum Section: String, Codable {
|
||||
case ancestors
|
||||
case descendants
|
||||
}
|
||||
|
||||
var statuses: QueryInterfaceRequest<StoredStatus> {
|
||||
request(for: Self.statuses)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,43 +402,7 @@ extension Filter: FetchableRecord, PersistableRecord {
|
|||
}
|
||||
}
|
||||
|
||||
private struct TransientStatusCollectionElement: Codable, FetchableRecord, PersistableRecord {
|
||||
let transientStatusCollectionId: String
|
||||
let statusId: String
|
||||
|
||||
static let status = belongsTo(StoredStatus.self, key: "statusId")
|
||||
}
|
||||
|
||||
extension TransientStatusCollection: StatusCollection {
|
||||
public var fetch: (Database) throws -> [StatusResult] {
|
||||
{
|
||||
try StatusResult.fetchAll(
|
||||
$0,
|
||||
StoredStatus.filter(
|
||||
try elements
|
||||
.fetchAll($0)
|
||||
.map(\.statusId)
|
||||
.contains(Column("id")))
|
||||
.including(required: StoredStatus.account)
|
||||
.including(optional: StoredStatus.reblogAccount)
|
||||
.including(optional: StoredStatus.reblog))
|
||||
}
|
||||
}
|
||||
|
||||
public func joinRecord(status: Status) -> PersistableRecord {
|
||||
TransientStatusCollectionElement(transientStatusCollectionId: id, statusId: status.id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TransientStatusCollection {
|
||||
static let elements = hasMany(TransientStatusCollectionElement.self)
|
||||
|
||||
var elements: QueryInterfaceRequest<TransientStatusCollectionElement> {
|
||||
request(for: Self.elements)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredStatus: Codable, Hashable {
|
||||
struct StoredStatus: Codable, Hashable {
|
||||
let id: String
|
||||
let uri: String
|
||||
let createdAt: Date
|
||||
|
@ -461,11 +494,11 @@ extension StoredStatus: FetchableRecord, PersistableRecord {
|
|||
}
|
||||
}
|
||||
|
||||
public struct StatusResult: Codable, Hashable, FetchableRecord {
|
||||
struct StatusResult: Codable, Hashable, FetchableRecord {
|
||||
let account: Account
|
||||
fileprivate let status: StoredStatus
|
||||
let status: StoredStatus
|
||||
let reblogAccount: Account?
|
||||
fileprivate let reblog: StoredStatus?
|
||||
let reblog: StoredStatus?
|
||||
}
|
||||
|
||||
private extension Status {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TransientStatusCollection: Codable {
|
||||
public let id: String
|
||||
|
||||
public init(id: String) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
|
@ -12,24 +12,12 @@ public struct ContextService {
|
|||
private let context = CurrentValueSubject<Context, Never>(Context(ancestors: [], descendants: []))
|
||||
private let networkClient: APIClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
private let collection: TransientStatusCollection
|
||||
|
||||
init(status: Status, networkClient: APIClient, contentDatabase: ContentDatabase) {
|
||||
self.status = status
|
||||
self.networkClient = networkClient
|
||||
self.contentDatabase = contentDatabase
|
||||
collection = TransientStatusCollection(id: "context-\(status.id)")
|
||||
statusSections = contentDatabase.statusesObservation(collection: collection)
|
||||
.combineLatest(context.setFailureType(to: Error.self))
|
||||
.map { statuses, context in
|
||||
[
|
||||
context.ancestors.map { a in statuses.first { $0.id == a.id } ?? a },
|
||||
[statuses.first { $0.id == status.id } ?? status],
|
||||
context.descendants.map { d in statuses.first { $0.id == d.id } ?? d }
|
||||
]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
statusSections = contentDatabase.contextObservation(parentID: status.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,12 +57,14 @@ extension ContextService: StatusListService {
|
|||
public func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
||||
Publishers.Merge(
|
||||
networkClient.request(StatusEndpoint.status(id: status.id))
|
||||
.map { ([$0], collection) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:)),
|
||||
.map { ([$0], nil) }
|
||||
.flatMap(contentDatabase.insert(statuses:timeline:))
|
||||
.eraseToAnyPublisher(),
|
||||
networkClient.request(ContextEndpoint.context(id: status.id))
|
||||
.handleEvents(receiveOutput: context.send)
|
||||
.map { ($0.ancestors + $0.descendants, collection) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:)))
|
||||
.map { ($0, status.id) }
|
||||
.flatMap(contentDatabase.insert(context:parentID:))
|
||||
.eraseToAnyPublisher())
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,7 @@ struct TimelineService {
|
|||
self.timeline = timeline
|
||||
self.networkClient = networkClient
|
||||
self.contentDatabase = contentDatabase
|
||||
statusSections = contentDatabase.statusesObservation(collection: timeline)
|
||||
.map { [$0] }
|
||||
statusSections = contentDatabase.statusesObservation(timeline: timeline)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +28,7 @@ extension TimelineService: StatusListService {
|
|||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
||||
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
|
||||
.map { ($0, timeline) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.flatMap(contentDatabase.insert(statuses:timeline:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ public extension StatusService {
|
|||
? StatusEndpoint.unfavourite(id: status.id)
|
||||
: StatusEndpoint.favourite(id: status.id))
|
||||
.map { ([$0], nil) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.flatMap(contentDatabase.insert(statuses:timeline:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue