-
-
Save gwynne/a71598ecb02d47253eb3896bcccb6963 to your computer and use it in GitHub Desktop.
Various Fluent Properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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