194 lines
5.6 KiB
Swift
194 lines
5.6 KiB
Swift
import FluentKit
|
|||
|
|
import Foundation
|
||
|
|
import HummingbirdFluent
|
||
|
|
|
||
|
|
/// Reads and writes what a collector keeps: their want list, and the series catalogue they fill gaps in.
|
||
|
|
public struct CollectingRepository: Sendable {
|
||
|
|
|
||
|
|
// MARK: Constants
|
||
|
|
|
||
|
|
/// The most wants an account keeps.
|
||
|
|
public static let maxWants = 50
|
||
|
|
|
||
|
|
// MARK: Properties
|
||
|
|
|
||
|
|
private let fluent: Fluent
|
||
|
|
|
||
|
|
/// The clock wants are stamped and new matches judged by.
|
||
|
|
private let now: @Sendable () -> Date
|
||
|
|
|
||
|
|
// MARK: Initializers
|
||
|
|
|
||
|
|
/// Creates a repository backed by the given `Fluent` service.
|
||
|
|
/// - Parameters:
|
||
|
|
/// - fluent: the service whose default database the repository operates on.
|
||
|
|
/// - now: the clock wants are stamped and new matches judged by; the system's by default.
|
||
|
|
public init(
|
||
|
|
fluent: Fluent,
|
||
|
|
now: @escaping @Sendable () -> Date = { .init() }
|
||
|
|
) {
|
||
|
|
self.fluent = fluent
|
||
|
|
self.now = now
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Wants
|
||
|
|
|
||
|
|
public extension CollectingRepository {
|
||
|
|
|
||
|
|
/// Adds a want to an account's list; a blank one, or one past the limit, is dropped.
|
||
|
|
func addWant(
|
||
|
|
title: String,
|
||
|
|
minGrade: Double?,
|
||
|
|
maxPriceCents: Int?,
|
||
|
|
slabbedOnly: Bool,
|
||
|
|
for user: User
|
||
|
|
) async throws {
|
||
|
|
let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
|
|
||
|
|
guard
|
||
|
|
!title.isEmpty,
|
||
|
|
try await WantRecord.query(on: fluent.db()).filter(\.$user.$id == user.id).count() < Self.maxWants
|
||
|
|
else {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let want = WantRecord()
|
||
|
|
|
||
|
|
want.$user.id = user.id
|
||
|
|
want.title = String(title.prefix(120))
|
||
|
|
want.minGrade = minGrade
|
||
|
|
want.maxPriceCents = maxPriceCents
|
||
|
|
want.slabbedOnly = slabbedOnly
|
||
|
|
want.alerts = true
|
||
|
|
want.createdAt = now()
|
||
|
|
// A match already listed is news to a want just made.
|
||
|
|
want.seenAt = .distantPast
|
||
|
|
|
||
|
|
try await want.save(on: fluent.db())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Marks an account's wants as looked at: their matches so far are no longer new.
|
||
|
|
func markWantsSeen(
|
||
|
|
by user: User
|
||
|
|
) async throws {
|
||
|
|
try await WantRecord.query(on: fluent.db())
|
||
|
|
.filter(\.$user.$id == user.id)
|
||
|
|
.set(\.$seenAt, to: now())
|
||
|
|
.update()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The number of new matches across the wants an account has alerts on for.
|
||
|
|
func newMatchCount(
|
||
|
|
for user: User
|
||
|
|
) async throws -> Int {
|
||
|
|
try await wants(of: user)
|
||
|
|
.filter(\.alerts)
|
||
|
|
.reduce(0) { $0 + $1.newMatchCount }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Removes one of an account's wants.
|
||
|
|
func removeWant(
|
||
|
|
_ id: UUID,
|
||
|
|
of user: User
|
||
|
|
) async throws {
|
||
|
|
try await WantRecord.query(on: fluent.db())
|
||
|
|
.filter(\.$id == id)
|
||
|
|
.filter(\.$user.$id == user.id)
|
||
|
|
.delete()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Switches the alerts of one of an account's wants.
|
||
|
|
func toggleAlerts(
|
||
|
|
ofWant id: UUID,
|
||
|
|
of user: User
|
||
|
|
) async throws {
|
||
|
|
guard let want = try await WantRecord.query(on: fluent.db())
|
||
|
|
.filter(\.$id == id)
|
||
|
|
.filter(\.$user.$id == user.id)
|
||
|
|
.first()
|
||
|
|
else {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
want.alerts.toggle()
|
||
|
|
|
||
|
|
try await want.save(on: fluent.db())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fetches an account's wants, the newest first, each with the listings that meet it.
|
||
|
|
func wants(
|
||
|
|
of user: User
|
||
|
|
) async throws -> [Want] {
|
||
|
|
let listings = try await CatalogueRepository(fluent: fluent).listings()
|
||
|
|
|
||
|
|
return try await WantRecord.query(on: fluent.db())
|
||
|
|
.filter(\.$user.$id == user.id)
|
||
|
|
.sort(\.$createdAt, .descending)
|
||
|
|
.all()
|
||
|
|
.map { record in
|
||
|
|
let filter = Want.filter(
|
||
|
|
title: record.title,
|
||
|
|
minGrade: record.minGrade,
|
||
|
|
maxPriceCents: record.maxPriceCents,
|
||
|
|
slabbedOnly: record.slabbedOnly
|
||
|
|
)
|
||
|
|
let matches = listings.filter(filter.matches)
|
||
|
|
|
||
|
|
return .init(
|
||
|
|
id: try record.requireID(),
|
||
|
|
title: record.title,
|
||
|
|
minGrade: record.minGrade,
|
||
|
|
maxPriceCents: record.maxPriceCents,
|
||
|
|
slabbedOnly: record.slabbedOnly,
|
||
|
|
alerts: record.alerts,
|
||
|
|
matches: matches,
|
||
|
|
newMatchCount: matches.count { $0.listedAt > record.seenAt }
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Series
|
||
|
|
|
||
|
|
public extension CollectingRepository {
|
||
|
|
|
||
|
|
/// Fetches every series in the catalogue, by name.
|
||
|
|
func allSeries() async throws -> [Series] {
|
||
|
|
try await SeriesRecord.query(on: fluent.db())
|
||
|
|
.sort(\.$name)
|
||
|
|
.all()
|
||
|
|
.map(\.snapshot)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fetches the series with the given slug.
|
||
|
|
///
|
||
|
|
/// A series sellers list copies of, but the catalogue has no run for, is made up from those listings — without an issue count, so without gaps.
|
||
|
|
func series(
|
||
|
|
slug: String
|
||
|
|
) async throws -> Series? {
|
||
|
|
if let record = try await SeriesRecord.query(on: fluent.db())
|
||
|
|
.filter(\.$slug == slug)
|
||
|
|
.first()
|
||
|
|
{
|
||
|
|
return record.snapshot
|
||
|
|
}
|
||
|
|
|
||
|
|
return try await CatalogueRepository(fluent: fluent)
|
||
|
|
.listings(matching: .init(sort: .newest))
|
||
|
|
.first { $0.seriesSlug == slug }
|
||
|
|
.map {
|
||
|
|
.init(
|
||
|
|
slug: slug,
|
||
|
|
name: $0.seriesName,
|
||
|
|
publisher: $0.publisher,
|
||
|
|
years: String($0.year),
|
||
|
|
issueCount: 0,
|
||
|
|
blurb: ""
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|