Skip to content
Pre-release
Pre-release

@tanner0101 tanner0101 released this Feb 25, 2020 · 23 commits to master since this release

Important, breaking changes highlighted with ⚠️.

Adds top-level FieldKey type (#165)

Adds a new top-level FieldKey type to FluentKit. This type represents possible field key values but in a slightly more type-safe way than String is currently able to.

The basic structure of FieldKey is a String literal expressible enum:

public enum FieldKey: ExpressibleByStringLiteral {
    case id
    case string(String)
}

FieldKey is usable everywhere String keys were previously usable, including property wrappers, filter expressions, and schema builders.

FieldKey can be extended by the end user. This could be a good solution to keeping String key declarations DRY. For example:

extension FieldKey {
    static var name: Self { "name" }
}

The new special id case should be used with the @ID property wrapper. The strings "id" and "_id" will both convert automatically to the special .id field key for backward compatibility.

⚠️ By default, @ID will now require the .id field key as well as UUID value. This allows for your models to support all of Fluent's drivers by default. This includes MongoDB.

There is a new syntax for using custom IDs:

@ID(custom: "foo", generatedBy: .user)
var id: String?

This syntax change is intended to push users toward declaring models that work with all drivers while still allowing usage of custom ID keys and types with limited driver support.

A new helper method for generating a default id column has been added to the schema builder:

database.schema("planets")
    .id()
    .field(...)
    ...
    .create()

Adds @Enum for using native database enums (#178).

Adds native enum support. This includes a new @Enum property, database.enum(_:) builder, and support for modifying a field's data type.

enum Bar: String, Codable {
    case baz, qux
}

Native database enums must be String backed since they are serialized directly to the query.

final class Foo: Model {
    static let schema = "foos"

    @ID(key: .id)
    var id: UUID?

    @Enum(key: "bar")
    var bar: Bar
}

If you are using a database with schemas, native enums must be registered using the new database.enum builder. This will create a data type that you can pass into the normal schema builder for the given field.

struct FooMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.enum("bar")
            .case("baz")
            .case("qux")
            .create()
            .flatMap
        { bar in
            database.schema("foos")
                .field("id", .uuid, .identifier(auto: false))
                .field("bar", bar, .required)
                .create()
        }
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema("foos").delete().flatMap {
            database.enum("bar").delete()
        }
    }
}

Adding cases to an enum is also supported:

enum Bar: String, Codable {
    case baz, qux, quuz
}

A migration is required to update the database:

struct BarAddQuuzMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.enum("bar")
            .case("quuz")
            .update()
            .flatMap
        { bar in
            database.schema("foos")
                .updateField("bar", bar)
                .update()
        }
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.enum("bar")
            .deleteCase("quuz")
            .update()
            .flatMap
        { bar in
            database.schema("foos")
                .updateField("bar", bar)
                .update()
        }
    }
}

Enum metadata is stored in the _fluent_enums schema.

Adds @CompoundField for storing nested fields as a flat, top-level fields in the database (#176).

This new property wrapper allows for a collection of fields to be stored as top-level keys in the database.

To use this feature, first declare a type representing the desired field structure conforming to Fields:

final class Pet: Fields {
    @Field(key: "name")
    var name: String

    @Field(key: "type")
    var type: Animal
}

Next, declare a @CompoundField on your model that uses this type.

final class User: Model {
    ...

    @CompoundField(key: "pet")
    var pet: Pet
}

This will result in Pet's fields being stored as top-level fields prefixed by "pet_" in the database, as can be seen by the migration:

struct UserMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users")
            .field("id", .uuid, .identifier(auto: false))
            .field("name", .string, .required)
            .field("pet_name", .string, .required)
            .field("pet_type", .string, .required)
            .create()
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users").delete()
    }
}

Compound fields can be accessed and queried from Swift using dot-syntax:

print(user.pet.name)
User.query(on: db).filter(\.$pet.$type == .cat)

Thanks to @Joannis and @calebkleveter for discovering how to do nested key paths!

Adds @NestedField for storing a collection of fields as a nested object in the database (#177).

@NestedField works similarly to @CompoundField except the fields are stored as a nested object in the database, not top-level keys.

This is reflected in migrations which should use the .json data type for @NestedField instead of many top-level fields for @CompoundField:

Replacing the @CompoundField example with @NestedField would require the following migration:

final class User: Model {
    ...

    @NestedField(key: "pet")
    var pet: Pet
}
private struct UserMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users")
            .field("id", .uuid, .identifier(auto: false))
            .field("name", .string, .required)
            .field("pet", .json, .required)
            .create()
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users").delete()
    }
}

DatabaseQuery aggregates have been refactored to be easier to implement in drivers (#171).

This change is mostly internal, except that FieldRepresentable has been split into two protocols named QueryField and FilterField. All fields that have a single key, like @ID, @Field, and @Parent are query fields. Filter fields are a special case of field that have a key path, like @NestedField. Filter fields may only be used with query builder operations that result in filters.

Set is now compatible with the .array database type (#170).

final class User: Model {
    @ID(key: .id)
    var id: UUID?

    @Field(key: "roles")
    var roles: Set<UserRole>
}
struct UserMigration: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users")
            .id()
            .field("roles", .array(of: .string), .required)
            .create()
    }

    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users").delete()
    }
}

MigrationLog renamed and uses UUID id for compatibility with MongoDB (#172).

⚠️ This is a breaking change for Vapor 4 databases with pre-existing migrations. Dropping your database and re-running migrations will generate the new log table correctly. To upgrade an existing log table while maintaining the data, you can use the following scripts:

MySQL

ALTER TABLE `fluent` DROP COLUMN `id`;
ALTER TABLE `fluent` ADD `id` VARBINARY(16);
UPDATE `fluent` SET `id` = uuid_to_bin(uuid());
ALTER TABLE `fluent` MODIFY COLUMN `id` VARBINARY(16) PRIMARY KEY;
RENAME TABLE `fluent` TO `_fluent_migrations`;

PostgreSQL

CREATE extension IF NOT EXISTS "uuid-ossp";
ALTER TABLE "fluent" DROP COLUMN "id";
ALTER TABLE "fluent" ADD "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4();
ALTER TABLE "fluent" ALTER COLUMN "id" DROP DEFAULT;
ALTER TABLE "fluent" RENAME TO "_fluent_migrations"

It is not possible to update a pre-existing SQLite database since SQLite does not expose a method for generating UUIDs.

FluentBenchmarker has been refactored to improve testing on all supported drivers (#170).

This change makes it easier to test drivers as well as ensuring that all benchmarker tests, even newly added ones, are running correctly.

Refactor DatabaseConfiguration and implement Databases.reinitialize(_:) (#174, #175).

This change is mostly an internal improvement to Fluent's database configuration code. It also allows for databases to be reinitialized, causing all open connections to be refreshed.

// Default database.
app.dbs.reinitialize()
// By database id.
app.dbs.reinitialize(.psql)

Improved ModelAlias support (#180).

ModelAlias is now more easily usable when building queries. It can be used anywhere a Model can be used.

The definition of a model alias has changed slightly to become:

final class HomeTeam: ModelAlias {
    static let name = "home_teams"
    let model = Team()
}
final class AwayTeam: ModelAlias {
    static let name = "away_teams"
    let model = Team()
}

Methods like filter and sort are now easier to use with aliases:

let matches = try Match.query(on: self.database)
    .join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id)
    .join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id)
    .filter(HomeTeam.self, \.$name == "a")
    .sort(AwayTeam.self, \.$name)
    .all().wait()

for match in matches {
    let home = try match.joined(HomeTeam.self)
    let away = try match.joined(AwayTeam.self)
    ...
}
Assets 2
You can’t perform that action at this time.