Created
June 17, 2024 13:21
-
-
Save zastrozzi/81713fec0251f4f6ca2b9711458347bf to your computer and use it in GitHub Desktop.
Fluent Enum Arrays
This file contains hidden or 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 Fluent | |
import PostgresNIO | |
import FluentPostgresDriver | |
import PostgresKit | |
extension Fields { | |
public typealias EnumArray<EnumValue> = EnumArrayProperty<Self, EnumValue> where EnumValue: FluentEnumConvertible | |
} | |
@propertyWrapper | |
public final class EnumArrayProperty<Model, EnumValue> where Model: FluentKit.Fields, EnumValue: FluentEnumConvertible { | |
public let field: FieldProperty<Model, Array<EnumValue>> | |
public var projectedValue: EnumArrayProperty<Model, EnumValue> { | |
return self | |
} | |
public var wrappedValue: Array<EnumValue> { | |
get { | |
guard let value = self.value else { | |
fatalError("Cannot access enum array field before it is initialized or fetched: \(self.field.key)") | |
} | |
return value | |
} | |
set { | |
self.value = newValue | |
} | |
} | |
public init(key: FieldKey) { | |
self.field = .init(key: key) | |
} | |
} | |
extension EnumArrayProperty: AnyProperty {} | |
extension EnumArrayProperty: Property { | |
public var value: Array<EnumValue>? { | |
get { | |
self.field.value.map { raw in | |
return raw | |
} | |
} | |
set { | |
self.field.value = newValue | |
} | |
} | |
} | |
extension EnumArrayProperty: AnyQueryableProperty { | |
public var path: [FieldKey] { | |
self.field.path | |
} | |
} | |
extension EnumArrayProperty: QueryableProperty { | |
public static func queryValue(_ value: Value) -> DatabaseQuery.Value { | |
.enumCase("{" + "\(value.map { $0.rawValue }.joined(separator: ","))" + "}") | |
} | |
} | |
extension EnumArrayProperty: AnyQueryAddressableProperty { | |
public var anyQueryableProperty: AnyQueryableProperty { self } | |
public var queryablePath: [FieldKey] { self.path } | |
} | |
extension EnumArrayProperty: QueryAddressableProperty { | |
public var queryableProperty: EnumArrayProperty<Model, EnumValue> { self } | |
} | |
extension EnumArrayProperty: AnyDatabaseProperty { | |
public var keys: [FieldKey] { | |
self.field.keys | |
} | |
public func input(to input: DatabaseInput) { | |
let value: DatabaseQuery.Value | |
if let fieldValue = self.field.value { | |
value = .array(fieldValue.map { .enumCase($0.rawValue) }) | |
} | |
else { value = .default } | |
switch value { | |
case .bind(let bind as String): | |
input.set(.enumCase(bind), at: self.field.key) | |
case .array(let items): | |
var transformedCases: [String] = [] | |
for i in items { | |
if case let .enumCase(caseString) = i { | |
transformedCases.append(caseString) | |
} | |
else if case let .bind(str as String) = i { | |
transformedCases.append(str) | |
} | |
} | |
input.set(.enumCase("{" + transformedCases.joined(separator: ",") + "}"), at: self.field.key) | |
case .default: | |
input.set(.default, at: self.field.key) | |
default: | |
fatalError("Unexpected input value type for '\(Model.self)'.'\(self.field.key)': \(value)") | |
} | |
} | |
public func output(from output: DatabaseOutput) throws { | |
if let stringOutOrig = try? output.decode(self.field.key, as: String.self), | |
stringOutOrig.hasPrefix("{"), | |
stringOutOrig.hasSuffix("}") | |
{ | |
var stringOut = stringOutOrig | |
stringOut.removeFirst(1) | |
stringOut.removeLast(1) | |
self.field.value = stringOut.split(separator: ",") | |
.compactMap { Value.Element.init(rawValue: String($0)) } as? Value ?? [] as! Value | |
} else { | |
try self.field.output(from: output) | |
} | |
} | |
} |
This file contains hidden or 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 PostgresKit | |
import PostgresNIO | |
public protocol FluentEnumConvertible: CaseIterable, RawRepresentable, PostgresArrayDecodable, Codable where Self.RawValue == String { | |
typealias Migration = ConvertibleEnumMigration<Self> | |
static var fluentEnumName: String { get } | |
} | |
extension FluentEnumConvertible { | |
public init<JSONDecoder: PostgresJSONDecoder>( | |
from buffer: inout ByteBuffer, | |
type: PostgresDataType, | |
format: PostgresFormat, | |
context: PostgresDecodingContext<JSONDecoder> | |
) throws { | |
let rawString = String(buffer: buffer) | |
guard let selfValue = Self.init(rawValue: rawString) else { | |
throw PostgresDecodingError.Code.failure | |
} | |
self = selfValue | |
} | |
public static func _decodeRaw<JSONDecoder: PostgresJSONDecoder>( | |
from byteBuffer: inout ByteBuffer?, | |
type: PostgresDataType, | |
format: PostgresFormat, | |
context: PostgresDecodingContext<JSONDecoder> | |
) throws -> Self { | |
guard var buffer = byteBuffer else { | |
throw PostgresDecodingError.Code.missingData | |
} | |
return try self.init(from: &buffer, type: type, format: format, context: context) | |
} | |
} | |
extension FluentEnumConvertible { | |
public static func toFluentEnum() -> DatabaseSchema.DataType.Enum { | |
return .init(name: Self.fluentEnumName, cases: allCases.map { $0.rawValue }) | |
} | |
} | |
public struct ConvertibleEnumMigration<ConvertibleEnum: FluentEnumConvertible>: AsyncMigration { | |
public func prepare(on database: Database) async throws { | |
do { | |
var newEnum = database.enum(ConvertibleEnum.fluentEnumName) | |
for enumCase in ConvertibleEnum.allCases { | |
newEnum = newEnum.case(enumCase.rawValue) | |
} | |
let _ = try await newEnum.create() | |
return | |
} | |
catch { | |
for enumCase in ConvertibleEnum.allCases { | |
do { | |
var newEnum = database.enum(ConvertibleEnum.fluentEnumName) | |
newEnum = newEnum.case(enumCase.rawValue) | |
let _ = try await newEnum.update() | |
continue | |
} catch { | |
continue | |
} | |
} | |
} | |
return | |
} | |
public func revert(on database: FluentKit.Database) async throws { | |
try await database.enum(ConvertibleEnum.fluentEnumName).delete() | |
} | |
public init() {} | |
} |
This file contains hidden or 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 | |
infix operator <~~ // Equivalent to <@ in postgres | |
infix operator ~~> // Equivalent to @> in postgres | |
infix operator <~> // Equivalent to && in postgres | |
// Does the first array contain the second, that is, does each element appearing in the second array equal some element of the first array? | |
public func <~~ <Model, Field, Values>( | |
lhs: KeyPath<Model, Field>, | |
rhs: Values | |
) -> ModelValueFilter<Model> where | |
Model: FluentKit.Schema, | |
Field: QueryableProperty, | |
Values: Collection, | |
Field.Value: Collection, | |
Values.Element == Field.Value.Element, | |
Values.Element: FluentEnumConvertible | |
{ | |
.init(lhs, .custom("<@"), .enumCase("{" + "\(rhs.map { $0.rawValue }.joined(separator: ","))" + "}")) | |
} | |
// Is the first array contained by the second? | |
public func ~~> <Model, Field, Values>( | |
lhs: KeyPath<Model, Field>, | |
rhs: Values | |
) -> ModelValueFilter<Model> where | |
Model: FluentKit.Schema, | |
Field: QueryableProperty, | |
Values: Collection, | |
Field.Value: Collection, | |
Values.Element == Field.Value.Element, | |
Values.Element: FluentEnumConvertible | |
{ | |
.init(lhs, .custom("@>"), .enumCase("{" + "\(rhs.map { $0.rawValue }.joined(separator: ","))" + "}")) | |
} | |
// Do the two arrays have any elements in common? | |
public func <~> <Model, Field, Values>( | |
lhs: KeyPath<Model, Field>, | |
rhs: Values | |
) -> ModelValueFilter<Model> where | |
Model: FluentKit.Schema, | |
Field: QueryableProperty, | |
Values: Collection, | |
Field.Value: Collection, | |
Values.Element == Field.Value.Element, | |
Values.Element: FluentEnumConvertible | |
{ | |
.init(lhs, .custom("&&"), .enumCase("{" + "\(rhs.map { $0.rawValue }.joined(separator: ","))" + "}")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment