Added the catalogue models and seed to support phase 4 of the POC prototype in the Persistence package.
This commit is contained in:
1 parent
0dc3fe6bea
commit
95e0a455e8
14 files changed
+743
-9
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import FluentKit
|
||||
import Foundation
|
||||
import HummingbirdFluent
|
||||
|
||||
/// Reads and creates accounts through the default database.
|
||||
@@ -25,6 +26,97 @@ public struct AccountRepository: Sendable {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Fetches the account with the given email, without creating one.
|
||||
/// - Returns: the account, or `nil` when no account has the email.
|
||||
public func existingAccount(
|
||||
email: String
|
||||
) async throws -> User? {
|
||||
try await UserRecord.query(on: fluent.db())
|
||||
.filter(\.$email == email)
|
||||
.first()?
|
||||
.snapshot
|
||||
}
|
||||
|
||||
/// Whether a credential identifier is still free to register.
|
||||
public func isUnregistered(
|
||||
credentialID: String
|
||||
) async throws -> Bool {
|
||||
try await PasskeyRecord.query(on: fluent.db())
|
||||
.filter(\.$credentialID == credentialID)
|
||||
.first() == nil
|
||||
}
|
||||
|
||||
/// Creates an account together with its first passkey: one step both joins and enrolls.
|
||||
///
|
||||
/// A passkey is only ever added to an account created in the same step. Adding one to an existing account would hand that account to
|
||||
/// whoever asks, since the prototype has no other proof of who is asking.
|
||||
/// - Returns: the new account, or `nil` when the email already has one.
|
||||
public func join(
|
||||
email: String,
|
||||
credentialID: String,
|
||||
publicKey: [UInt8],
|
||||
signCount: UInt32
|
||||
) async throws -> User? {
|
||||
try await fluent.db().transaction { database in
|
||||
guard try await UserRecord.query(on: database)
|
||||
.filter(\.$email == email)
|
||||
.first() == nil
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let user = UserRecord(
|
||||
email: email,
|
||||
displayName: String(email.prefix { $0 != "@" })
|
||||
)
|
||||
|
||||
try await user.save(on: database)
|
||||
try await PasskeyRecord(
|
||||
userID: try user.requireID(),
|
||||
credentialID: credentialID,
|
||||
publicKey: publicKey,
|
||||
signCount: signCount,
|
||||
at: .init()
|
||||
).save(on: database)
|
||||
|
||||
return try user.snapshot
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the passkey with the given credential identifier, and the account it signs in.
|
||||
public func passkey(
|
||||
credentialID: String
|
||||
) async throws -> Passkey? {
|
||||
try await PasskeyRecord.query(on: fluent.db())
|
||||
.filter(\.$credentialID == credentialID)
|
||||
.with(\.$user)
|
||||
.first()?
|
||||
.snapshot
|
||||
}
|
||||
|
||||
/// Fetches the passkeys registered to the account with the given email.
|
||||
public func passkeys(
|
||||
email: String
|
||||
) async throws -> [Passkey] {
|
||||
try await PasskeyRecord.query(on: fluent.db())
|
||||
.join(UserRecord.self, on: \PasskeyRecord.$user.$id == \UserRecord.$id)
|
||||
.filter(UserRecord.self, \.$email == email)
|
||||
.with(\.$user)
|
||||
.all()
|
||||
.map { try $0.snapshot }
|
||||
}
|
||||
|
||||
/// Records a passkey's signature counter after a sign-in.
|
||||
public func recordUse(
|
||||
ofPasskey credentialID: String,
|
||||
signCount: UInt32
|
||||
) async throws {
|
||||
try await PasskeyRecord.query(on: fluent.db())
|
||||
.filter(\.$credentialID == credentialID)
|
||||
.set(\.$signCount, to: Int(signCount))
|
||||
.update()
|
||||
}
|
||||
|
||||
/// Fetches the account with the given email, creating it when there is none: one step both signs in and joins.
|
||||
///
|
||||
/// A new account is named after the email's local part until its owner picks a display name.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import FluentKit
|
||||
import Foundation
|
||||
import HummingbirdFluent
|
||||
|
||||
/// Reads the shops and their listings through the default database.
|
||||
@@ -49,6 +50,48 @@ public struct CatalogueRepository: Sendable {
|
||||
.sorted(by: filter.ordersBefore)
|
||||
}
|
||||
|
||||
/// Publishes a draft as a listing of the shop the seller owns, under a slug of its own.
|
||||
/// - Parameters:
|
||||
/// - draft: what the seller filled in.
|
||||
/// - seller: the account listing the copy.
|
||||
/// - date: the moment the copy is listed; the present by default.
|
||||
/// - Returns: the listing, or `nil` when the seller owns no shop.
|
||||
public func publish(
|
||||
_ draft: ListingDraft,
|
||||
by seller: User,
|
||||
at date: Date = .init()
|
||||
) async throws -> Listing? {
|
||||
try await fluent.db().transaction { database in
|
||||
guard let shop = try await ShopRecord.query(on: database)
|
||||
.filter(\.$owner.$id == seller.id)
|
||||
.first()
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let listing = draft.listing(slug: "", shop: shop.snapshot, at: date)
|
||||
let base = ListingDraft.slug("\(listing.title) \(listing.gradeLabel)")
|
||||
let taken = Set(
|
||||
try await ListingRecord.query(on: database)
|
||||
.filter(\.$slug =~ base)
|
||||
.all(\.$slug)
|
||||
)
|
||||
var slug = base
|
||||
var suffix = 2
|
||||
|
||||
while taken.contains(slug) {
|
||||
slug = "\(base)-\(suffix)"
|
||||
suffix += 1
|
||||
}
|
||||
|
||||
let published = draft.listing(slug: slug, shop: shop.snapshot, at: date)
|
||||
|
||||
try await ListingRecord(published, shopID: try shop.requireID()).save(on: database)
|
||||
|
||||
return published
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the name of every publisher with a listing, in alphabetical order.
|
||||
public func publishers() async throws -> [String] {
|
||||
try await ListingRecord.query(on: fluent.db())
|
||||
|
||||
@@ -472,7 +472,7 @@ private extension TradeRepository {
|
||||
sale.declaredGrade = declared.gradeLabel + (declared.certNumber.map { " · cert \($0)" } ?? "")
|
||||
sale.declaredPageQuality = declared.pageQuality
|
||||
sale.declaredPrinting = declared.printingDetail
|
||||
sale.declaredNotes = declared.sellerNotes
|
||||
sale.declaredNotes = declared.declaration
|
||||
sale.agreedAt = now()
|
||||
|
||||
listing.soldAt = now()
|
||||
|
||||
Reference in new issue
Block a user