Skip to content

Instantly share code, notes, and snippets.

@gwynne
Last active June 3, 2024 16:28
Show Gist options
  • Save gwynne/a71598ecb02d47253eb3896bcccb6963 to your computer and use it in GitHub Desktop.
Save gwynne/a71598ecb02d47253eb3896bcccb6963 to your computer and use it in GitHub Desktop.
Various Fluent Properties
import FluentKit
import Foundation
// MARK: - "Can be aliased" property capability
/// For the sake of excessive completeness (and, as an excuse, for consistency with Fluent's existing
/// plethora of "this property supports ..." protocols), this protocol provides the type-erased
/// version of ``AliasableProperty``, in the same way that `AnyProperty` is the type-erased
/// form of `Property`.
///
/// It is decidedly questionable whether this protocol serves any purpose whatsoever other than to
/// allow generically checking whether a given property can be aliased (which is itself of questionable
/// value, for that matter). Certainly the type-erased value accessors are pointless, as best I can
/// figure at the time of this writing (hopefully, I will be proven wrong eventually).
public protocol AnyAliasableProperty: FluentKit.AnyProperty {
/// Type-erased form of ``AliasableProperty/wrappedValue``.
var anyWrappedValue: Any { get }
/// Type-erased form of ``AliasableProperty/projectedValue``.
var anyProjectedValue: AnyAliasableProperty { get }
}
/// A simple protocol to make a `Property`'s `projectedValue` and `wrappedValue` accessors
/// available generically.
///
/// The need for this is unfortunate, especially given that it's technically impossible for there
/// to be a property type that _isn't_ aliasable, but in a particularly convoluted twist of the rules
/// governing API breakage, `Property` can't be safely extended to add these accessors, with or without
/// FluentKit's cooperation - doing it from inside Fluent would be an API break due to the change in
/// conformance requirements, and extending it from outside Fluent doesn't correctly call into the
/// property's accessors (the generic defaulted implementations are always called) - and that in turn
/// bypasses the checks many property types perform in their `wrappedValue` getters, which makes some of
/// them behave unexpectedly. The only way out of this would be to have the ability to recognize property
/// wrappers via the typesystem (e.g. an implicit `PropertyWrapper` protocol, similar to `GlobalActor`),
/// which isn't going to happen, or to break public Fluent API.
public protocol AliasableProperty: AnyAliasableProperty, FluentKit.Property {
/// A few of the property types don't follow the usual convention of returning `Value?` from their
/// `value` accessors and `Value` from their ``wrappedValue`` accessors - in particular, `IDProperty`
/// and `CompositeIDProperty` return the same type (with the same optionality) in both places, due to
/// the convoluted semantics of a model's ID always being optional on the Swift side. Therefore we
/// provide an `associatedtype` on the protocol, allowing the actual return type of the accessor,
/// including the correct amount of optionality, to be inferred automatically by the compiler. The
/// protocol is already non-existential anyhow, and aliasing only cares about knowing what the correct
/// type is, not whether it matches up, so nothing is lost with this approach.
associatedtype AliasedValueType: Codable
/// An ``AliasableProperty``, like any property wrapper, has a ``wrappedValue``. The property wrapper's
/// own `wrappedValue` accessor will automatically fulfill this protocol requirement, and the compiler
/// will automatically infer ``AliasedValueType`` to be whatever the wrapper's accessor returned.
var wrappedValue: AliasedValueType { get set }
/// All Fluent property types are required to return `self` as their projected value, so we just require
/// that ``projectedValue`` have `Self` as its type, which will always be correct for each property type.
var projectedValue: Self { get }
}
/// ``AliasableProperty``s get automatic ``AnyAliasableProperty`` conformance by default.
extension AnyAliasableProperty where Self: AliasableProperty {
/// Default implementation of ``AnyAliasableProperty/anyWrappedValue`` via ``AliasableProperty/wrappedValue``.
public var anyWrappedValue: Any { self.wrappedValue }
/// Default implementation of ``AnyAliasableProperty/anyProjectedValue`` via ``AliasableProperty/projectedValue``.
public var anyProjectedValue: AnyAliasableProperty { self }
}
// MARK: - "Alias another property" property wrapper
extension Fields {
/// See ``AliasProperty`` for description and usage.
public typealias Alias<Original> = AliasProperty<Self, Original>
where Original: AliasableProperty
/// See ``UnwrappingAliasProperty`` for description and usage.
public typealias UnwrappingAlias<Original> = UnwrappingAliasProperty<Self, Original>
where Original: AliasableProperty, Original.AliasedValueType == Original.Value?
}
/// A property wrapper which aliases a property to an existing FluentKit property
/// elsewhere on (or deeper within) the same model. Primarily useful for simplfying
/// access to the ID properties of a model using `@CompositeID`.
///
/// Example (with some boilerplate omitted):
/// ```swift
/// final class CompositeModel: Model, @unchecked Sendable {
/// final class IDValue: Fields, Hashable, @unchecked Sendable {
/// @Parent(key: "id") var parent: ParentModel
/// @Field(key: "index") var index: Int
/// }
///
/// @CompositeID var id: IDValue?
///
/// @Alias(of: \.$id.$parent) var parent: ParentModel
/// @Alias(of: \.$id.$index) var index: Int
/// }
/// ```
///
/// > Note: ``AliasProperty`` is unique among property wrappers intended for use with Fluent models
/// > in that it does not itself conform to `Property`, or even `AnyProperty`. This is a deliberate
/// > choice which prevents the presence of aliases from confusing Fluent's internal behavior, as it
/// > means they don't appear even in a model's `properties` array.
@propertyWrapper
public struct AliasProperty<Model, Original>: Sendable
where Model: FluentKit.Fields, Original: AliasableProperty
{
/// A key path pointing to the targeted property.
public typealias TargetKeyPath = KeyPath<Model, Original>
/// A key path pointing to this property's backing storage property.
public typealias StorageKeyPath = ReferenceWritableKeyPath<Model, Self>
/// The key path of the targeted property.
public let targetKeyPath: TargetKeyPath
/// Forward `wrappedValue` accesses through the owner object using the target keypath.
///
/// The generic `Value` parameter allows the wrapped value access to nest into the
/// targeted property.
///
/// - Parameters:
/// - owner: The object which contains the property wrapper being accessed.
/// - wrapped: A key path pointing to the synthesized value property for the property wrapper.
/// - storage: A key path pointing to the backing store property for the property wrapper.
/// - Returns: The result of forwarding the access to the target property's `wrappedValue` property.
public static subscript<Value>(
_enclosingInstance owner: Model,
wrapped wrapped: ReferenceWritableKeyPath<Model, Value>,
storage storage: StorageKeyPath
) -> Original.AliasedValueType {
get { owner[keyPath: owner[keyPath: storage].targetKeyPath].wrappedValue }
set { owner[keyPath: owner[keyPath: storage].targetKeyPath].wrappedValue = newValue }
}
/// Forward `projectedValue` reads through the owner object using the target keypath.
///
/// - Parameters:
/// - owner: The object which contains the property wrapper being accessed.
/// - projected: A key path pointing to the synthesized projected value property for the property wrapper.
/// - storage: A key path pointing to the backing store property for the property wrapper.
/// - Returns: The result of forwarding the access to the target property's `projectedValue` property.
public static subscript(
_enclosingInstance owner: Model,
projected projected: TargetKeyPath,
storage storage: StorageKeyPath
) -> Original {
owner[keyPath: owner[keyPath: storage].targetKeyPath].projectedValue
}
/// Create an ``AliasProperty`` targeting the property at the given key path on the same model.
public init(of targetKeyPath: KeyPath<Model, Original>) {
self.targetKeyPath = targetKeyPath
}
/// The `wrappedValue` accessors must be present in order to enable usage of the the
/// ``subscript(_enclosingInstance:wrapped:storage:)`` subscript. It is both a compile
/// and runtime error for these accessors to be referenced or invoked. This also ensures
/// that this property wrapper can only be applied to reference types.
@available(*, unavailable, message: "Only usable within reference types")
public var wrappedValue: Original.AliasedValueType {
get { fatalError() }
set { fatalError() }
}
/// The `projectedValue` accessor must be present in order to enable usage of the the
/// ``subscript(_enclosingInstance:projected:storage:)`` subscript. It is both a compile
/// and runtime error for this accessor to be referenced or invoked. This also ensures
/// that this property wrapper can only be applied to reference types.
@available(*, unavailable, message: "Only usable within reference types")
public var projectedValue: Original {
fatalError()
}
}
/// Exactly the same as ``AliasProperty``, except the targeted property's value must be optional and
/// the resulting value when the alias is accessed is the force-unwrapped value of the targeted property.
@propertyWrapper
public struct UnwrappingAliasProperty<Model, Original>
where Model: FluentKit.Fields, Original: AliasableProperty, Original.AliasedValueType == Original.Value?
{
/// A key path pointing to the targeted property.
public typealias TargetKeyPath = KeyPath<Model, Original>
/// A key path pointing to this property's backing storage property.
public typealias StorageKeyPath = ReferenceWritableKeyPath<Model, Self>
/// The key path of the targeted property.
public let targetKeyPath: TargetKeyPath
/// Forward `wrappedValue` accesses through the owner object using the target keypath.
///
/// The generic `Value` parameter allows the wrapped value access to nest into the
/// targeted property.
///
/// - Parameters:
/// - owner: The object which contains the property wrapper being accessed.
/// - wrapped: A key path pointing to the synthesized value property for the property wrapper.
/// - storage: A key path pointing to the backing store property for the property wrapper.
/// - Returns: The result of forwarding the access to the target property's `wrappedValue` property.
public static subscript<Value>(
_enclosingInstance owner: Model,
wrapped wrapped: ReferenceWritableKeyPath<Model, Value>,
storage storage: StorageKeyPath
) -> Original.AliasedValueType.Wrapped {
get { owner[keyPath: owner[keyPath: storage].targetKeyPath].wrappedValue! }
set { owner[keyPath: owner[keyPath: storage].targetKeyPath].wrappedValue = newValue }
}
/// Forward `projectedValue` reads through the owner object using the target keypath.
///
/// - Parameters:
/// - owner: The object which contains the property wrapper being accessed.
/// - projected: A key path pointing to the synthesized projected value property for the property wrapper.
/// - storage: A key path pointing to the backing store property for the property wrapper.
/// - Returns: The result of forwarding the access to the target property's `projectedValue` property.
public static subscript(
_enclosingInstance owner: Model,
projected projected: TargetKeyPath,
storage storage: StorageKeyPath
) -> Original {
owner[keyPath: owner[keyPath: storage].targetKeyPath].projectedValue
}
/// Create an ``UnwrappingAliasProperty`` targeting the property at the given key path on the same model.
public init(of targetKeyPath: KeyPath<Model, Original>) {
self.targetKeyPath = targetKeyPath
}
/// The `wrappedValue` accessors must be present in order to enable usage of the the
/// ``subscript(_enclosingInstance:wrapped:storage:)`` subscript. It is both a compile
/// and runtime error for these accessors to be referenced or invoked. This also ensures
/// that this property wrapper can only be applied to reference types.
@available(*, unavailable, message: "Only usable within reference types")
public var wrappedValue: Original.AliasedValueType.Wrapped {
get { fatalError() }
set { fatalError() }
}
/// The `projectedValue` accessor must be present in order to enable usage of the the
/// ``subscript(_enclosingInstance:projected:storage:)`` subscript. It is both a compile
/// and runtime error for this accessor to be referenced or invoked. This also ensures
/// that this property wrapper can only be applied to reference types.
@available(*, unavailable, message: "Only usable within reference types")
public var projectedValue: Original {
fatalError()
}
}
// MARK: - Add aliasability to existing property types
/// Extend all of the existing FluentKit property types with conformance to the new protocol.
///
/// Having to explicitly add the conformance to any additional property types anyone else might
/// have implemented elsewhere is another reason it's unfortunate that we need the protocol.
///
/// > Note: ``AliasProperty`` and ``UnwrappingAliasProperty`` are omitted from this list deliberately;
/// > they are the only properties which are _not_ aliasable, partially due to their lack of conformance
/// > the `Property` protocol. (It's probably possible to make aliases that can be chained, but it doesn't
/// > seem worth the effort.)
extension FluentKit.IDProperty: AliasableProperty {}
extension FluentKit.CompositeIDProperty: AliasableProperty {}
extension FluentKit.FieldProperty: AliasableProperty {}
extension FluentKit.OptionalFieldProperty: AliasableProperty {}
extension FluentKit.BooleanProperty: AliasableProperty {}
extension FluentKit.OptionalBooleanProperty: AliasableProperty {}
extension FluentKit.EnumProperty: AliasableProperty {}
extension FluentKit.OptionalEnumProperty: AliasableProperty {}
extension FluentKit.TimestampProperty: AliasableProperty {}
extension FluentKit.ParentProperty: AliasableProperty {}
extension FluentKit.OptionalParentProperty: AliasableProperty {}
extension FluentKit.CompositeParentProperty: AliasableProperty {}
extension FluentKit.CompositeOptionalParentProperty: AliasableProperty {}
extension FluentKit.ChildrenProperty: AliasableProperty {}
extension FluentKit.OptionalChildProperty: AliasableProperty {}
extension FluentKit.CompositeChildrenProperty: AliasableProperty {}
extension FluentKit.CompositeOptionalChildProperty: AliasableProperty {}
extension FluentKit.SiblingsProperty: AliasableProperty {}
extension FluentKit.GroupProperty: AliasableProperty {}
import FluentKit
// MARK: - @FlatGroup
extension Model {
/// See ``FlatGroupProperty``.
public typealias FlatGroup<Value> = FlatGroupProperty<Self, Value>
where Value: Fields
}
// MARK: - FlatGroupProperty
/// `@FlatGroup` embeds the Fluent properties found on `Value` in `Model`, as if those
/// properties had been specified on `Model`. It is very similar to `@Group`, except
/// no prefix gets added to the properties.
///
/// Example:
/// ```swift
/// final class CommonData: Fields {
/// @Field(key: "a") var a: String
/// @Field(key: "b") var b: Int
///
/// init() {}
/// }
///
/// final class FirstModel: Model {
/// static let schema = "first_models"
/// @ID(custom: .id) var id: Int
/// @Group(key: "first") var data: CommonData
///
/// init() {}
/// }
///
/// final class SecondModel: Model {
/// static let schema = "second_models"
/// @ID(custom: .id) var id: Int
/// @FlatGroup var data: CommonData
///
/// init() {}
/// }
///
/// print(FirstModel.keys)
/// // ["id", "first_a", "first_b"]
///
/// print(SecondModel.keys)
/// // ["id", "a", "b"]
/// ```
///
/// - Warning: It is easier than usual to accidentally end up with duplicate key names in
/// a model when using `@FlatGroup`. If this happens, don't let it stay that way! Even if
/// everything appears to work correctly at first, it will eventually cause enough confusion
/// in Fluent to have unpredictable results.
@propertyWrapper @dynamicMemberLookup
public final class FlatGroupProperty<Model, Value>
where Model: FluentKit.Fields, Value: FluentKit.Fields
{
/// The underlying storage of the properties nested on ``Value``. It is optional
/// only because ``FluentKit/Property``'s ``value`` requirement must always be
/// optional. It will never actually be `nil` in practice at runtime.
public var value: Value?
/// Fluent property wrappers must return `self` as their ``projectedValue``.
public var projectedValue: FlatGroupProperty<Model, Value> { self }
/// The accessor invoked by the compiler when the wrapped property is read or set
/// by calling code. As noted on ``value`` above, the actual value is never supposed
/// to be `nil`, so the wrapped value type is non-optional, and encountering `nil`
/// anyway immediately results in a fatal runtime error.
public var wrappedValue: Value {
get { self.value! } // If you crash here, you've misused this property; this should never be nil
set { self.value = newValue }
}
/// `@FlatGroup` has no parameters, so the initializer is pretty simple.
public init() {
self.value = .init()
}
/// Enable accessing the projected values of properties defined on `Value` via our own projected value.
///
/// Example:
/// ```swift
/// final class CommonFields: Fields {
/// @Field(key: "a")
/// var a: String
///
/// @Parent(key: "b")
/// var b: BModel
///
/// init() {}
/// }
///
/// final class AModel: Model {
/// static let schema = "amodels"
///
/// @ID(custom: .id)
/// var id: Int?
///
/// @FlatGroup
/// var commonFields: CommonFields
///
/// init() {}
/// }
///
/// let models = try await AModel.query(on: database)
/// .filter(\.$commonFields.$a == "a")
/// .all()
/// ```
///
/// Unlike `@Group`, `@FlatGroup` does not perform any transformations on the keys of its nested
/// properties. Thus, while `\.commonFields.$a` and `\.$commonFields.$a` would mean two different
/// things if `commonFields` was a `@Group`, they are effectively identical when using `@FlatGroup`.
///
/// - Tip: For the sake of consistency and clarity, it is considered best practice for calling code
/// to use the same syntax that would otherwise be required for a `@Group`. (Short short version:
/// `\.$foo.$bar` and `\.foo.bar` are good, `\.foo.$bar` is bad.)
public subscript<Nested>(dynamicMember keyPath: KeyPath<Value, Nested>) -> Nested where Nested: Property {
self.value![keyPath: keyPath]
}
}
// MARK: - CustomStringConvertible
/// Provide `@FlatGroup` properties with a description which includes all of the relevant types in the result.
extension FlatGroupProperty: CustomStringConvertible {
/// See ``Swift/CustomStringConvertible/description``.
public var description: String {
"@\(Model.self).FlatGroup<\(Value.self))>()"
}
}
// MARK: - [Any]Property
/// `@FlatGroup` is intended for use with properties of ``FluentKit/Fields`` et al.
extension FlatGroupProperty: AnyProperty, Property {}
// MARK: - AnyDatabaseProperty
/// `@FlatGroup` has content that interacts with database queries (i.e. the properties it contains).
extension FlatGroupProperty: AnyDatabaseProperty {
/// See ``FluentKit/AnyDatabaseProperty/keys``.
public var keys: [FieldKey] {
Value.keys
}
/// See ``FluentKit/AnyDatabaseProperty/input(to:)``.
public func input(to input: DatabaseInput) {
self.value!.input(to: input)
}
/// See ``FluentKit/AnyDatabaseProperty/output(from:)``.
public func output(from output: DatabaseOutput) throws {
try self.value!.output(from: output)
}
}
// MARK: - AnyCodableProperty
/// `@FlatGroup` participates in ``FluentKit/Fields``'s ``Swift/Codable`` conformance (i.e. by
/// passing it on to its contained properties).
extension FlatGroupProperty: AnyCodableProperty {
/// See ``FluentKit/AnyCodableProperty/encode(to:)``.
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.value!)
}
/// See ``FluentKit/AnyCodableProperty/decode(from:)``.
public func decode(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try .some(container.decode(Value.self))
}
}
//extension FlatGroupProperty: AliasableProperty {}
import NIOCore
import FluentKit
extension Model {
public typealias KeySpecificParent<To> = KeySpecificParentProperty<Self, To>
where To: FluentKit.Model
}
@propertyWrapper
public final class KeySpecificParentProperty<From, To>
where From: Model, To: Model
{
@ParentProperty<From, To.IDValue>
public var parent: To.IDValue
public var wrappedValue: To { self.$parent.wrappedValue }
public var projectedValue: KeySpecificParentProperty<From, To> { self }
public var value: To? { get { self.$parent.value } set { self.$parent.value = newValue } }
public init() { self._parent = .init(key: "the_hardcoded_key") }
public func query(on database: any Database) -> QueryBuilder<To> { self.$parent.query(on: database) }
}
extension KeySpecificParentProperty: CustomStringConvertible {
public var description: String { self.$parent.description }
}
extension KeySpecificParentProperty: Relation {
public var name: String { self.$parent.name }
public func load(on database: any Database) -> EventLoopFuture<Void> { self.$parent.load(on: database) }
}
extension KeySpecificParentProperty: AnyProperty {}
extension KeySpecificParentProperty: Property {
public typealias Model = From
public typealias Value = To
}
extension KeySpecificParentProperty: AnyQueryAddressableProperty {
public var anyQueryableProperty: AnyQueryableProperty { self.$parent.anyQueryableProperty }
public var queryablePath: [FieldKey] { self.$parent.queryablePath }
}
extension KeySpecificParentProperty: QueryAddressableProperty {
public var queryableProperty: FieldProperty<From, To.IDValue> { self.$parent.queryableProperty }
}
extension KeySpecificParentProperty: AnyDatabaseProperty {
public var keys: [FieldKey] { self.$parent.keys }
public func input(to input: DatabaseInput) { self.$parent.input(to: input) }
public func output(from output: DatabaseOutput) throws { try self.$parent.output(from: output) }
}
extension KeySpecificParentProperty: AnyCodableProperty {
public func encode(to encoder: Encoder) throws { try self.$parent.encode(to: encoder) }
public func decode(from decoder: Decoder) throws { try self.$parent.decode(from: decoder) }
}
extension KeySpecificParentProperty: EagerLoadable {
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, KeySpecificParentProperty<From, To>>,
to builder: Builder
)
where Builder : EagerLoadBuilder, From == Builder.Model
{ self.eagerLoad(relationKey, withDeleted: false, to: builder) }
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, From.KeySpecificParent<To>>,
withDeleted: Bool,
to builder: Builder
)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = KeySpecificParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}
public static func eagerLoad<Loader, Builder>(
_ loader: Loader,
through: KeyPath<From, From.KeySpecificParent<To>>,
to builder: Builder
) where
Loader: EagerLoader,
Loader.Model == To,
Builder: EagerLoadBuilder,
Builder.Model == From
{
let loader = ThroughKeySpecificParentEagerLoader(relationKey: through, loader: loader)
builder.add(loader: loader)
}
}
private struct KeySpecificParentEagerLoader<From, To>: EagerLoader
where From: FluentKit.Model, To: FluentKit.Model
{
let relationKey: KeyPath<From, KeySpecificParentProperty<From, To>>
let withDeleted: Bool
func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
let sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
let builder = To.query(on: database).filter(\._$id ~~ Set(sets.keys))
if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })
for (parentId, models) in sets {
guard let parent = parents[parentId] else {
database.logger.debug(
"Missing parent model in eager-load lookup results.",
metadata: ["parent": .string("\(To.self)"), "id": .string("\(parentId)")]
)
throw FluentError.missingParentError(keyPath: self.relationKey, id: parentId)
}
models.forEach { $0[keyPath: self.relationKey].value = parent }
}
}
}
}
private struct KeySpecificThroughParentEagerLoader<From, Through, Loader>: EagerLoader
where From: Model, Loader: EagerLoader, Loader.Model == Through
{
let relationKey: KeyPath<From, From.KeySpecificParent<Through>>
let loader: Loader
func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
let throughs = models.map {
$0[keyPath: self.relationKey].value!
}
return self.loader.run(models: throughs, on: database)
}
}
//extension KeySpecificParentProperty: AliasableProperty {}
import NIOCore
import FluentKit
import struct SQLKit.SomeCodingKey
extension Model {
public typealias OptionalPointer<To, ToProp> = OptionalPointerProperty<Self, To, ToProp>
where To: FluentKit.Model, ToProp: QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
}
// MARK: Type
@propertyWrapper
public final class OptionalPointerProperty<From, To, ToProp>: @unchecked Sendable
where From: Model, To: Model, ToProp: QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
{
@OptionalFieldProperty<From, ToProp.Value>
public var ref: ToProp.Value?
public var wrappedValue: To? {
get {
self.value ?? nil
}
set { fatalError("use $ prefix to access \(self.name)") }
}
public var projectedValue: OptionalPointerProperty<From, To, ToProp> {
self
}
let toKeypath: KeyPath<To, ToProp>
public var value: To??
public init(key: FieldKey, pointsTo relatedKeypath: KeyPath<To, ToProp>) {
self._ref = .init(key: key)
self.toKeypath = relatedKeypath
}
public func query(on database: any Database) -> QueryBuilder<To> {
let builder = To.query(on: database)
if let ref = self.ref {
builder.filter(self.toKeypath == ref)
} else {
builder.filter(self.toKeypath == .null)
}
return builder
}
}
extension OptionalPointerProperty: CustomStringConvertible {
public var description: String {
self.name
}
}
// MARK: Relation
extension OptionalPointerProperty: Relation {
public var name: String {
"OptionalPointer<\(From.self), \(To.self), \(To()[keyPath: self.toKeypath].path[0].description)>(key: \(self.$ref.key))"
}
public func load(on database: any Database) -> EventLoopFuture<Void> {
self.query(on: database).first().map {
self.value = $0
}
}
}
// MARK: Property
extension OptionalPointerProperty: AnyProperty { }
extension OptionalPointerProperty: Property {
public typealias Model = From
public typealias Value = To?
}
// MARK: Query-addressable
extension OptionalPointerProperty: AnyQueryAddressableProperty {
public var anyQueryableProperty: any AnyQueryableProperty { self.$ref.anyQueryableProperty }
public var queryablePath: [FieldKey] { self.$ref.queryablePath }
}
extension OptionalPointerProperty: QueryAddressableProperty {
public var queryableProperty: OptionalFieldProperty<From, ToProp.Value> { self.$ref.queryableProperty }
}
// MARK: Database
extension OptionalPointerProperty: AnyDatabaseProperty {
public var keys: [FieldKey] {
self.$ref.keys
}
public func input(to input: any DatabaseInput) {
self.$ref.input(to: input)
}
public func output(from output: any DatabaseOutput) throws {
try self.$ref.output(from: output)
}
}
extension OptionalPointerProperty: AnyCodableProperty {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
if case .some(.some(let pointer)) = self.value {
try container.encode(pointer)
} else {
try container.encode([
"ref": self.ref
])
}
}
public func decode(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: SQLKit.SomeCodingKey.self)
try self.$ref.decode(from: container.superDecoder(forKey: .init(stringValue: "ref")))
}
}
// MARK: Eager Loadable
extension OptionalPointerProperty: EagerLoadable {
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, OptionalPointerProperty<From, To, ToProp>>,
to builder: Builder
)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, From.OptionalPointer<To, ToProp>>,
withDeleted: Bool,
to builder: Builder
)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = OptionalPointerEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}
public static func eagerLoad<Loader, Builder>(
_ loader: Loader,
through: KeyPath<From, From.OptionalPointer<To, ToProp>>,
to builder: Builder
) where
Loader: EagerLoader,
Loader.Model == To,
Builder: EagerLoadBuilder,
Builder.Model == From
{
let loader = ThroughOptionalPointerEagerLoader(relationKey: through, loader: loader)
builder.add(loader: loader)
}
}
private struct OptionalPointerEagerLoader<From, To, ToProp>: EagerLoader
where From: FluentKit.Model, To: FluentKit.Model, ToProp: FluentKit.QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
{
let relationKey: KeyPath<From, OptionalPointerProperty<From, To, ToProp>>
let withDeleted: Bool
func run(models: [From], on database: any Database) -> EventLoopFuture<Void> {
let toKeypath = From()[keyPath: self.relationKey].toKeypath
var _sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].ref })
let nilPointerModels = _sets.removeValue(forKey: nil) ?? []
let sets = _sets
if sets.isEmpty {
// Fetching "To" objects is unnecessary when no models have an id for "To".
nilPointerModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
return database.eventLoop.makeSucceededVoidFuture()
}
let builder = To.query(on: database).filter(toKeypath ~~ Set(sets.keys.compactMap { $0 }))
if self.withDeleted {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let pointers = Dictionary(uniqueKeysWithValues: $0.map { ($0[keyPath: toKeypath].value!, $0) })
for (pointerRef, models) in sets {
guard let pointer = pointers[pointerRef!] else {
database.logger.debug(
"Missing pointer-referred model in eager-load lookup results.",
metadata: ["pointer": .string("\(To.self)"), "ref": .string("\(pointerRef!)")]
)
throw FluentError.missingParent(from: "\(From.self)", to: "\(To.self)", key: From.path(for: self.relationKey.appending(path: \.$ref))[0].description, id: "\(pointerRef!)")
}
models.forEach { $0[keyPath: self.relationKey].value = .some(.some(pointer)) }
}
nilPointerModels.forEach { $0[keyPath: self.relationKey].value = .some(.none) }
}
}
}
private struct ThroughOptionalPointerEagerLoader<From, Through, ThroughProp, Loader>: EagerLoader
where From: Model, Loader: EagerLoader, Loader.Model == Through, ThroughProp: QueryableProperty, ThroughProp.Model == Through, ThroughProp.Value: Hashable
{
let relationKey: KeyPath<From, From.OptionalPointer<Through, ThroughProp>>
let loader: Loader
func run(models: [From], on database: any Database) -> EventLoopFuture<Void> {
self.loader.run(models: models.compactMap { $0[keyPath: self.relationKey].value! }, on: database)
}
}
import NIOCore
import FluentKit
import struct SQLKit.SomeCodingKey
extension Model {
public typealias Pointer<To, ToProp> = PointerProperty<Self, To, ToProp>
where To: FluentKit.Model, ToProp: QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
}
// MARK: Type
@propertyWrapper
public final class PointerProperty<From, To, ToProp>: @unchecked Sendable
where From: Model, To: Model, ToProp: QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
{
@FieldProperty<From, ToProp.Value>
public var ref: ToProp.Value
public var wrappedValue: To {
get {
guard let value = self.value else {
fatalError("Parent relation not eager loaded, use $ prefix to access: \(self.name)")
}
return value
}
set { fatalError("use $ prefix to access \(self.name)") }
}
public var projectedValue: PointerProperty<From, To, ToProp> {
return self
}
let toKeypath: KeyPath<To, ToProp>
public var value: To?
public init(key: FieldKey, pointsTo relatedKeypath: KeyPath<To, ToProp>) {
self._ref = .init(key: key)
self.toKeypath = relatedKeypath
}
public func query(on database: any Database) -> QueryBuilder<To> {
return To.query(on: database)
.filter(self.toKeypath == self.ref)
}
}
extension PointerProperty: CustomStringConvertible {
public var description: String {
self.name
}
}
// MARK: Relation
extension PointerProperty: Relation {
public var name: String {
"Pointer<\(From.self), \(To.self), \(To()[keyPath: self.toKeypath].path[0].description)>(key: \(self.$ref.key))"
}
public func load(on database: any Database) -> EventLoopFuture<Void> {
self.query(on: database).first().map {
self.value = $0
}
}
}
// MARK: Property
extension PointerProperty: AnyProperty { }
extension PointerProperty: Property {
public typealias Model = From
public typealias Value = To
}
// MARK: Query-addressable
extension PointerProperty: AnyQueryAddressableProperty {
public var anyQueryableProperty: any AnyQueryableProperty { self.$ref.anyQueryableProperty }
public var queryablePath: [FieldKey] { self.$ref.queryablePath }
}
extension PointerProperty: QueryAddressableProperty {
public var queryableProperty: FieldProperty<From, ToProp.Value> { self.$ref.queryableProperty }
}
// MARK: Database
extension PointerProperty: AnyDatabaseProperty {
public var keys: [FieldKey] {
self.$ref.keys
}
public func input(to input: any DatabaseInput) {
self.$ref.input(to: input)
}
public func output(from output: any DatabaseOutput) throws {
try self.$ref.output(from: output)
}
}
extension PointerProperty: AnyCodableProperty {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
if let parent = self.value {
try container.encode(parent)
} else {
try container.encode([
"ref": self.ref
])
}
}
public func decode(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: SQLKit.SomeCodingKey.self)
try self.$ref.decode(from: container.superDecoder(forKey: .init(stringValue: "ref")))
}
}
// MARK: Eager Loadable
extension PointerProperty: EagerLoadable {
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, PointerProperty<From, To, ToProp>>,
to builder: Builder
)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, From.Pointer<To, ToProp>>,
withDeleted: Bool,
to builder: Builder
)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = PointerEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}
public static func eagerLoad<Loader, Builder>(
_ loader: Loader,
through: KeyPath<From, From.Pointer<To, ToProp>>,
to builder: Builder
) where
Loader: EagerLoader,
Loader.Model == To,
Builder: EagerLoadBuilder,
Builder.Model == From
{
let loader = ThroughPointerEagerLoader(relationKey: through, loader: loader)
builder.add(loader: loader)
}
}
private struct PointerEagerLoader<From, To, ToProp>: EagerLoader
where From: FluentKit.Model, To: FluentKit.Model, ToProp: FluentKit.QueryableProperty, ToProp.Model == To, ToProp.Value: Hashable
{
let relationKey: KeyPath<From, PointerProperty<From, To, ToProp>>
let withDeleted: Bool
func run(models: [From], on database: any Database) -> EventLoopFuture<Void> {
let toKeypath = From()[keyPath: self.relationKey].toKeypath
let sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].ref })
let builder = To.query(on: database).filter(toKeypath ~~ Set(sets.keys))
if self.withDeleted {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let pointers = Dictionary(uniqueKeysWithValues: $0.map { ($0[keyPath: toKeypath].value!, $0) })
for (pointerRef, models) in sets {
guard let pointer = pointers[pointerRef] else {
database.logger.debug(
"Missing pointer-referred model in eager-load lookup results.",
metadata: ["pointer": .string("\(To.self)"), "ref": .string("\(pointerRef)")]
)
throw FluentError.missingParent(from: "\(From.self)", to: "\(To.self)", key: From.path(for: self.relationKey.appending(path: \.$ref))[0].description, id: "\(pointerRef)")
}
models.forEach { $0[keyPath: self.relationKey].value = pointer }
}
}
}
}
private struct ThroughPointerEagerLoader<From, Through, ThroughProp, Loader>: EagerLoader
where From: Model, Loader: EagerLoader, Loader.Model == Through, ThroughProp: QueryableProperty, ThroughProp.Model == Through, ThroughProp.Value: Hashable
{
let relationKey: KeyPath<From, From.Pointer<Through, ThroughProp>>
let loader: Loader
func run(models: [From], on database: any Database) -> EventLoopFuture<Void> {
self.loader.run(models: models.map { $0[keyPath: self.relationKey].value! }, on: database)
}
}
//extension PointerProperty: AliasableProperty {}
import FluentKit
import struct Foundation.Date
extension Model {
public typealias RequiredTimestamp<Format> = RequiredTimestampProperty<Self, Format>
where Format: TimestampFormat
}
@propertyWrapper
public final class RequiredTimestampProperty<Model, Format>
where Model: FluentKit.Model, Format: TimestampFormat
{
@FieldProperty<Model, Format.Value>
public var timestamp: Format.Value
let format: Format
public var projectedValue: RequiredTimestampProperty<Model, Format> { self }
public var wrappedValue: Date {
get {
guard let value = self.value else {
fatalError("Cannot access field before it is initialized or fetched: \(self.$timestamp.key)")
}
return value
}
set { self.value = newValue }
}
public convenience init(key: FieldKey, format: TimestampFormatFactory<Format>) {
self.init(key: key, format: format.makeFormat())
}
public init(key: FieldKey, format: Format) {
self._timestamp = .init(key: key)
self.format = format
}
}
extension RequiredTimestampProperty where Format == DefaultTimestampFormat {
public convenience init(key: FieldKey) {
self.init(key: key, format: .default)
}
}
extension RequiredTimestampProperty: CustomStringConvertible {
public var description: String { "@\(Model.self).RequiredTimestamp(key: \(self.$timestamp.key))" }
}
extension RequiredTimestampProperty: AnyProperty {}
extension RequiredTimestampProperty: Property {
public var value: Date? {
get { self.$timestamp.value.flatMap(self.format.parse(_:)) }
set { self.$timestamp.value = newValue.flatMap(self.format.serialize(_:)) }
}
}
extension RequiredTimestampProperty: AnyQueryableProperty {
public var path: [FieldKey] { self.$timestamp.path }
}
extension RequiredTimestampProperty: QueryableProperty {}
extension RequiredTimestampProperty: AnyDatabaseProperty {
public var keys: [FieldKey] { self.$timestamp.keys }
public func input(to input: any DatabaseInput) { self.$timestamp.input(to: input) }
public func output(from output: any DatabaseOutput) throws { try self.$timestamp.output(from: output) }
}
extension RequiredTimestampProperty: AnyCodableProperty {
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.wrappedValue)
}
public func decode(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode(Date?.self)
}
}
extension RequiredTimestampProperty: AnyQueryAddressableProperty {
public var anyQueryableProperty: any AnyQueryableProperty { self }
public var queryablePath: [FieldKey] { self.path }
}
extension RequiredTimestampProperty: QueryAddressableProperty {
public var queryableProperty: RequiredTimestampProperty<Model, Format> { self }
}
//extension RequiredTimestampProperty: AliasableProperty {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment