Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

26. Database/API Versioning & Migration
Written by Jonas Schwartz

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

In the first three sections of the book, whenever you made a change to your model, you had to delete your database and start over. That’s no problem when you don’t have any data. Once you have data, or move your project to the production stage, you can no longer delete your database. What you want to do instead is modify your database, which in Vapor, is done using migrations.

Note: This chapter requires that you have set up and configured PostgreSQL. Follow the steps in Chapter 6, “Configuring a Database”, to set up PostgreSQL in Docker and configure the Vapor application.

In this chapter, you’ll make two modifications to the TILApp using migrations. First, you’ll add a new field to User to contain a Twitter handle. Second, you’ll ensure that categories are unique. Finally, you’re going to modify the app so it creates the admin user only when your app runs in development or testing mode.

Note: The version of TILApp provided for this chapter’s sample files is not the complete version from the end of Section 3. Instead, it’s a simplified, earlier iteration. You can integrate these changes in your working copy of the project, if you wish.

Modifying tables

Modifying an existing database is always a risky business. You already have data you don’t want to lose, so deleting the whole database is not a viable solution. At the same time, you can’t simply add or remove a property in an existing table since all the data is entangled in one big web of connections and relations.

Instead, you introduce your modifications using Vapor’s Migration protocol. This allows you to cautiously introduce your modifications while still having a revert option should they not work as expected.

Modifying your production database is always a delicate procedure. You must make sure to test any modifications properly before rolling them out in production. If you have a lot of important data, it’s a good idea to take a backup before modifying your database.

To keep your code clean and make it easy to view the changes in chronological order, you should create a directory containing all your migrations. Each migration should have its own file. For file names, use a consistent and helpful naming scheme, for example: YY-MM-DD-FriendlyName.swift. This allows you to see the versions of your database at a glance.

Writing migrations

A Migration is generally written as a struct when it’s used to update an existing model. This struct must, of course, conform to Migration. Migration requires you to provide three things:

typealias Database: Fluent.Database

static func prepare(
  on connection: Database.Connection) -> Future<Void>

static func revert(
  on connection: Database.Connection) -> Future<Void>

Typealias Database

First, you must specify what type of database the migration can run on. Migrations require a database connection to work correctly as they must be able to query the MigrationLog model. If the MigrationLog is not accessible, the migration will fail and, in the worst case, break your application.

Prepare method

prepare(on:) contains the migration’s changes to the database. It’s usually one of two options:

static func prepare(
  on connection: PostgreSQLConnection) -> Future<Void> {
  // 1
  return Database.create(
    NewTestUser.self,
    on: connection) { builder in
      // 2
      builder.field(for: \.id, isIdentifier: true)
  }
}

Revert method

revert(on:) is the opposite of prepare(on:). Its job is to undo whatever prepare(on:) did. If you use create(_:on:closure:) in prepare(on:), you use delete(_:on:) in revert(on:). If you use update(_:on:closure:) to add a field, you also use it in revert(on:) to remove the field with deleteField(for:).

static func revert(
  on connection: PostgreSQLConnection) -> Future<Void> {
    return Database.delete(NewTestUser.self,
                           on: connection)
}

Adding users’ Twitter handles

To demonstrate the migration process for an existing database, you’re going to add support for collecting and storing users’ Twitter handles. First, you need to create a new folder to hold all your migrations and a new file to hold the AddTwitterToUser migration. In Terminal, navigate to the directory which holds your TILApp project and enter:

# 1
mkdir Sources/App/Migrations
# 2
touch Sources/App/Migrations/18-06-05-AddTwitterToUser.swift
# 3
vapor xcode -y
var twitterURL: String?
init(name: String,
     username: String,
     password: String,
     twitterURL: String? = nil) {
  self.name = name
  self.username = username
  self.password = password
  self.twitterURL = twitterURL
}

Creating the migration

When you use a migration to add a new property to an existing model, it’s important you modify the initial migration so that it adds only the original fields. By default, prepare(on:) adds every property it finds in the model. If, for some reason — running your test suite, for example — you revert your entire database, allowing it to continue to add all fields in the initial migration will cause your new migration to fail.

builder.field(for: \.id, isIdentifier: true)
builder.field(for: \.name)
builder.field(for: \.username)
builder.field(for: \.password)
import FluentPostgreSQL
import Vapor

// 1
struct AddTwitterURLToUser: Migration {

  // 2  
  typealias Database = PostgreSQLDatabase

  // 3  
  static func prepare(
  	on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 4
    return Database.update(
      User.self, 
      on: connection
    ) { builder in
      // 5
      builder.field(for: \.twitterURL)
    }
  }

  // 6  
  static func revert(
  	on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 7
    return Database.update(
      User.self, 
      on: connection
    ) { builder in
      // 8
      builder.deleteField(for: \.twitterURL)
    }
  }
}
migrations.add(
  migration: AddTwitterURLToUser.self,
  database: .psql)
docker exec -it postgres psql -U vapor
\d "User"
\q

Versioning the API

You’ve changed the model to include the user’s Twitter handle but you haven’t altered the existing API. While you could simply update the API to include the Twitter handle, this might break existing consumers of your API. Instead, you can create a new API version to return users with their Twitter handles.

final class PublicV2: Codable {
  var id: UUID?
  var name: String
  var username: String
  var twitterURL: String?

  init(id: UUID?, 
       name: String, 
       username: String, 
       twitterURL: String? = nil) {
    self.id = id
    self.name = name
    self.username = username
    self.twitterURL = twitterURL
  }
}
extension User.PublicV2: Content {}
func convertToPublicV2() -> User.PublicV2 {
  return User.PublicV2(
  	id: id, 
  	name: name, 
  	username: username, 
  	twitterURL: twitterURL)
}
func convertToPublicV2() -> Future<User.PublicV2> {
  return self.map(to: User.PublicV2.self) { user in
    return user.convertToPublicV2()
  }
}
 // 1
func getV2Handler(_ req: Request) throws 
    -> Future<User.PublicV2> {
  // 2
  return try req.parameters.next(User.self).convertToPublicV2()
}
// API Version 2 Routes
 // 1
let usersV2Route = router.grouped("api", "v2", "users")
 // 2
usersV2Route.get(User.parameter, use: getV2Handler)

Updating the web site

Your app now has all it needs to store a user’s Twitter handle and the API is complete. You need to update the web site to allow a new user to provide a Twitter address during the registration process.

<div class="form-group">
  <label for="twitterURL">Twitter handle</label>
  <input type="text" name="twitterURL" class="form-control"
   id="twitterURL"/>
</div>
<h2>#(user.username)
#if(user.twitterURL) {
 - #(user.twitterURL)
}
</h2>
let twitterURL: String?
let user = User(
  name: data.name,
  username: data.username,
  password: password)
var twitterURL: String?
if 
  let twitter = data.twitterURL,
  !twitter.isEmpty {
    twitterURL = twitter
}
let user = User(
  name: data.name,
  username: data.username,
  password: password,
  twitterURL: twitterURL)

Making categories unique

Just as you’ve required usernames to be unique, you really want category names to be unique as well. Everything you’ve done so far to implement categories has made it impossible to create duplicates but you’d like that enforced in the database as well. It’s time to create a Migration that guarantees duplicate category names can’t be inserted in the database.

touch Sources/App/Migrations/18-06-05-MakeCategoriesUnique.swift
vapor xcode -y
import FluentPostgreSQL
import Vapor
// 1
struct MakeCategoriesUnique: Migration {
  // 2  
  typealias Database = PostgreSQLDatabase
  // 3  
  static func prepare(
    on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 4
    return Database.update(
      Category.self,
      on: connection
    ) { builder in
      // 5
      builder.unique(on: \.name)
    }
  }
  // 6  
  static func revert(
    on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 7
    return Database.update(
      Category.self,
      on: connection
    ) { builder in
      // 8
      builder.deleteUnique(from: \.name)
    }
  }
}
migrations.add(
  migration: MakeCategoriesUnique.self,
  database: .psql)

Seeding based on environment

In Chapter 18, “API Authentication, Part 1,” you seeded an admin user in your database. As mentioned there, you should never use “password” as your admin password. But, it’s easier when you’re still developing and just need a dummy account for testing locally. One way to ensure you don’t add this user in production is to detect your environment before adding the migration. In configure.swift replace:

migrations.add(migration: AdminUSer.self, database: .psql)
switch env {
case .development, .testing:
  migrations.add(migration: AdminUser.self, database: .psql)
default:
  break
}

Where to go from here?

In this chapter, you learned how to modify your database after your app enters production using migrations. You saw how to add an extra property — twitterUrl — to User, how to revert this update, and how to enforce uniqueness of category names. Finally, you saw how to switch on your environment in configure.swift, allowing you to exclude migrations from the production environment.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now