Skip to content

Instantly share code, notes, and snippets.

@zastrozzi
Created June 17, 2024 13:21
Show Gist options
  • Save zastrozzi/81713fec0251f4f6ca2b9711458347bf to your computer and use it in GitHub Desktop.
Save zastrozzi/81713fec0251f4f6ca2b9711458347bf to your computer and use it in GitHub Desktop.
Fluent Enum Arrays
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)
}
}
}
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() {}
}
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