Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Last active May 30, 2020 14:25
Show Gist options
  • Save maximkrouk/10126931f43694cf0950025032362bdf to your computer and use it in GitHub Desktop.
Save maximkrouk/10126931f43694cf0950025032362bdf to your computer and use it in GitHub Desktop.
DEPRECATED
import Fluent
private struct _Migration<T>: Migration {
var preparation: (Database) -> EventLoopFuture<Void>
var revertion: (Database) -> EventLoopFuture<Void>
func prepare(on database: Database) -> EventLoopFuture<Void> { preparation(database) }
func revert(on database: Database) -> EventLoopFuture<Void> { revertion(database) }
}
public protocol MigrationProvider {
static func prepare(on database: Database) -> EventLoopFuture<Void>
static func revert(on database: Database) -> EventLoopFuture<Void>
}
extension MigrationProvider {
public static var migration: Migration {
_Migration<Self>(preparation: prepare, revertion: revert)
}
}
public protocol DatabaseModel: Model, MigrationProvider {
typealias SchemaBuilder = SchemaBuilderProxy<Self>
static func prepareSchema(using schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void>
static func revertSchema(using schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void>
}
extension DatabaseModel {
public static func prepare(on database: Database) -> EventLoopFuture<Void> {
prepareSchema(using: database.schema(for: Self.self))
}
public static func revert(on database: Database) -> EventLoopFuture<Void> {
revertSchema(using: database.schema(for: Self.self))
}
}
//
// SchemaBuilder+Extension.swift
// App
//
// Created by Maxim Krouk on 3/17/20.
//
import Fluent
// MARK: - Pass by reference
@propertyWrapper
private final class Ref<Object> {
public var wrappedValue: Object
public convenience init(_ object: Object) {
self.init(wrappedValue: object)
}
public init(wrappedValue: Object) {
self.wrappedValue = wrappedValue
}
}
@dynamicMemberLookup
public struct SchemaBuilderProxy<T: Fluent.Model> {
@Ref private var wrappedValue: SchemaBuilder
/// Empty model, used to access fields by keypath
private let model: T = .init()
// MARK: Initialization
internal init(wrappedValue: SchemaBuilder) {
self._wrappedValue = .init(wrappedValue)
}
// MARK: DynamicMember
public subscript<T>(dynamicMember keyPath: KeyPath<SchemaBuilder, T>) -> T {
get { wrappedValue[keyPath: keyPath] }
}
public subscript<T>(dynamicMember keyPath: WritableKeyPath<SchemaBuilder, T>) -> T {
get { wrappedValue[keyPath: keyPath] }
set { wrappedValue[keyPath: keyPath] = newValue }
}
// MARK: - FieldRepresentableSupport
public func field<V: FieldRepresentable>(
_ keyPath: KeyPath<T, V>,
_ dataType: DatabaseSchema.DataType,
_ constraints: DatabaseSchema.FieldConstraint...
) -> Self {
self.field(.definition(
name: .string(model[keyPath: keyPath].field.key),
dataType: dataType,
constraints: constraints
))
}
public func unique<V: FieldRepresentable>(on keyPaths: KeyPath<T, V>...) -> Self {
wrappedValue.schema.constraints.append(.unique(
fields: keyPaths.map { .string(model[keyPath: $0].field.key) }
))
return self
}
public func foreignKey<V: FieldRepresentable>(
_ keyPath: KeyPath<T, V>,
references foreignSchema: String,
_ foreignField: String,
onDelete: DatabaseSchema.Constraint.ForeignKeyAction = .noAction,
onUpdate: DatabaseSchema.Constraint.ForeignKeyAction = .noAction
) -> Self {
self.foreignKey(
model[keyPath: keyPath].field.key,
references: foreignSchema,
foreignField,
onDelete: onDelete,
onUpdate: onUpdate
)
}
public func deleteField<V: FieldRepresentable>(_ keyPath: KeyPath<T, V>) -> Self {
deleteField(model[keyPath: keyPath].field.key)
}
}
// MARK: - SchemaBuilderProxy.SchemeBuilderSupport
extension SchemaBuilderProxy {
public func field(
_ name: String,
_ dataType: DatabaseSchema.DataType,
_ constraints: DatabaseSchema.FieldConstraint...
) -> Self {
field(.definition(
name: .string(name),
dataType: dataType,
constraints: constraints
))
}
public func field(_ field: DatabaseSchema.FieldDefinition) -> Self {
wrappedValue.schema.createFields.append(field)
return self
}
public func unique(on fields: String...) -> Self {
wrappedValue.schema.constraints.append(.unique(
fields: fields.map { .string($0) }
))
return self
}
public func foreignKey(
_ field: String,
references foreignSchema: String,
_ foreignField: String,
onDelete: DatabaseSchema.Constraint.ForeignKeyAction = .noAction,
onUpdate: DatabaseSchema.Constraint.ForeignKeyAction = .noAction
) -> Self {
wrappedValue.schema.constraints.append(.foreignKey(
fields: [.string(field)],
foreignSchema: foreignSchema,
foreignFields: [.string(foreignField)],
onDelete: onDelete,
onUpdate: onUpdate
))
return self
}
public func deleteField(_ name: String) -> Self {
deleteField(.string(name))
}
public func deleteField(_ name: DatabaseSchema.FieldName) -> Self {
wrappedValue.schema.deleteFields.append(name)
return self
}
public func delete() -> EventLoopFuture<Void> {
wrappedValue.delete()
}
public func update() -> EventLoopFuture<Void> {
wrappedValue.update()
}
public func create() -> EventLoopFuture<Void> {
wrappedValue.create()
}
}
// MARK: - SchemaBuilder.Binding
extension SchemaBuilder {
public func bind<T: Model>(to type: T.Type) -> SchemaBuilderProxy<T> {
.init(wrappedValue: self)
}
}
// MARK: - Model.Schema
extension Model {
static var schema: String { String(describing: self) }
}
// MARK: - Database.Schema
extension Database {
public func schema<T: Model>(for type: T.Type) -> SchemaBuilderProxy<T> {
schema(T.schema).bind(to: type)
}
}
@maximkrouk
Copy link
Author

maximkrouk commented Mar 17, 2020

Usage

Project.swift

import Fluent
import Vapor

// some model
final class Project: DatabaseModel, Content {
    static let schema = "projects"
    
    @ID(key: "id")
    var id: String?

    @Field(key: "title")
    var title: String
    
    @Field(key: "description")
    var description: String
    
    @Parent(key: "owner_id")
    var owner: User
    
    @Children(for: \.$project)
    var questions: [Question]

    init() { }

    init(id: String? = nil, title: String, description: String, ownerID: UUID) {
        self.id = id
        self.title = title
        self.description = description
        self.$owner.id = ownerID
    }
}

extension Project: MigrationProvider {
    // prepare database with typo-safe schemaBuilder
    static func prepareSchema(using schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void> {
        schemaBuilder
            .field(\.$id, .string, .identifier(auto: false))
            .field(\.$title, .string, .identifier(auto: false))
            .field(\.$description, .string, .identifier(auto: false))
            .field(\.$owner, .uuid, .identifier(auto: false))
            .create()
    }
    
    // revert database with typo-safe schemaBuilder
    static func revertSchema(using schemaBuilder: SchemaBuilder) -> EventLoopFuture<Void> {
        schemaBuilder.delete()
    }
}

configure.swift

public func configure(_ app: Application) throws {
    // ...
    // add migrations
    app.migrations.add(Project.migration)

    // force run migrations if needed
    try app.migrator.setupIfNeeded().wait()
    try app.migrator.prepareBatch().wait()
    // ...
}

Back to index

@maximkrouk
Copy link
Author

BTW migrations should not be tight to models, so the implementation may cause issues and now it's DEPRECATED

@maximkrouk
Copy link
Author

maximkrouk commented Mar 30, 2020

The issue with your implementation is the reason why models and migrations have been decoupled in the first place - as soon as you change a column name, or delete a column then it's going to blow up
© 0xTim

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment