Actualizar a 4.0¶
Esta guía muestra cómo actualizar un proyecto existente de Vapor 3.x a 4.x. Esta guía intenta cubrir todos los paquetes oficiales de Vapor, así como algunos providers de uso común. Si nota que falta algo, el chat del equipo de Vapor es un excelente lugar para pedir ayuda. También se agradecen issues y pull request.
Dependencias¶
Para usar Vapor 4, necesitarás Xcode 11.4 y macOS 10.15 o superior.
La sección Instalación de los documentos analiza la instalación de dependencias.
Package.swift¶
El primer paso para actualizar a Vapor 4 es actualizar las dependencias de su proyecto. A continuación se muestra un ejemplo de un archivo Package.swift actualizado. También puedes consultar la plantilla Package.swift actualizada.
-// swift-tools-version:4.0
+// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "api",
+ platforms: [
+ .macOS(.v10_15),
+ ],
dependencies: [
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
- .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
+ .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
- .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
+ .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
],
targets: [
.target(name: "App", dependencies: [
- "FluentPostgreSQL",
+ .product(name: "Fluent", package: "fluent"),
+ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
- "Vapor",
+ .product(name: "Vapor", package: "vapor"),
- "JWT",
+ .product(name: "JWT", package: "jwt"),
]),
- .target(name: "Run", dependencies: ["App"]),
- .testTarget(name: "AppTests", dependencies: ["App"])
+ .target(name: "Run", dependencies: [
+ .target(name: "App"),
+ ]),
+ .testTarget(name: "AppTests", dependencies: [
+ .target(name: "App"),
+ ])
]
)
Todos los paquetes que se hayan actualizado para Vapor 4 tendrán su número de versión principal incrementado en uno.
Advertencia
El identificador de prelanzamiento -rc
se utiliza ya que algunos paquetes de Vapor 4 aún no se han actualizado oficialmente.
Paquetes Antiguos¶
Algunos paquetes de Vapor 3 han quedado obsoletos, como por ejemplo:
vapor/auth
: Ahora incluido en Vapor.vapor/core
: Absorbido en varios módulos.vapor/crypto
: Reemplazado por SwiftCrypto (Ahora incluido en Vapor).vapor/multipart
: Ahora incluido en Vapor.vapor/url-encoded-form
: Ahora incluido en Vapor.vapor-community/vapor-ext
: Ahora incluido en Vapor.vapor-community/pagination
: Ahora parte de Fluent.IBM-Swift/LoggerAPI
: Reemplazado por SwiftLog.
Dependencia Fluent¶
vapor/fluent
ahora debe agregarse como una dependencia separada a su lista de dependencias y targets. Todos los paquetes específicos de bases de datos tienen el sufijo -driver
para aclarar el requisito de vapor/fluent
.
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
Plataformas¶
Los manifiestos del paquete de Vapor ahora son explícitamente compatibles con macOS 10.15 y superiores. Esto significa que tu paquete también deberá especificar la compatibilidad con la plataforma.
+ platforms: [
+ .macOS(.v10_15),
+ ],
Vapor puede agregar plataformas compatibles adicionales en el futuro. Tu paquete puede admitir cualquier subconjunto de estas plataformas siempre que el número de versión sea igual o mayor a los requisitos mínimos de versión de Vapor.
Xcode¶
Vapor 4 utiliza SPM nativo de Xcode 11. Esto significa que ya no necesitarás generar archivos .xcodeproj
. Al abrir la carpeta de tu proyecto en Xcode, se reconocerá automáticamente SPM y se incorporarán las dependencias.
Puedes abrir tu proyecto de forma nativa en Xcode usando vapor xcode
o open Package.swift
.
Una vez que hayas actualizado Package.swift, es posible que debas cerrar Xcode y borrar las siguientes carpetas del directorio raíz:
Package.resolved
.build
.swiftpm
*.xcodeproj
Una vez que tus paquetes actualizados se hayan resuelto exitosamente, deberías ver errores del compilador, probablemente bastantes. ¡No te preocupes! Te mostraremos cómo solucionarlos.
Run¶
Lo primero que debemos hacer es actualizar el archivo main.swift
de tu módulo Run al nuevo formato.
import App
import Vapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()
El contenido del archivo main.swift
reemplaza al app.swift
del módulo de aplicación, por lo que puedes eliminar ese archivo.
Aplicación¶
Echemos un vistazo a cómo actualizar la estructura básica del módulo de la aplicación.
configure.swift¶
El método configure
debe cambiarse para aceptar una instancia de Application
.
- public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
+ public func configure(_ app: Application) throws
A continuación se muestra un ejemplo de un método de configuración actualizado.
import Fluent
import FluentSQLiteDriver
import Vapor
// Llamado antes de que se inicialice su aplicación.
public func configure(_ app: Application) throws {
// Sirve archivos del directorio `Public/`
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
// Configura la base de datos SQLite
app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
// Configura migraciones
app.migrations.add(CreateTodo())
try routes(app)
}
A continuación se mencionan los cambios de sintaxis para configurar cosas como routing, middleware, fluent y más.
boot.swift¶
El contenido de boot
se puede colocar en el método configure
ya que ahora acepta la instancia de la aplicación.
routes.swift¶
El método routes
debe cambiarse para aceptar una instancia de Application
.
- public func routes(_ router: Router, _ container: Container) throws
+ public func routes(_ app: Application) throws
A continuación se menciona más información sobre los cambios en la sintaxis de routing.
Servicios¶
Las APIs de servicios de Vapor 4 se han simplificado para que resulte más fácil descubrirlos y utilizarlos. Los servicios ahora están expuestos como métodos y propiedades en Application
y Request
, lo que permite al compilador ayudarte en su uso.
Para entender esto mejor, echemos un vistazo a algunos ejemplos.
// Cambiar el puerto predeterminado del servidor a 8281
- services.register { container -> NIOServerConfig in
- return .default(port: 8281)
- }
+ app.http.server.configuration.port = 8281
En lugar de registrar un NIOServerConfig
en los servicios, la configuración del servidor ahora se expone como propiedades simples en Application que se pueden anular.
// Registrar middleware cors
let corsConfiguration = CORSMiddleware.Configuration(
allowedOrigin: .all,
allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
)
let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
- var middlewares = MiddlewareConfig() // Create _empty_ middleware config
- middlewares.use(corsMiddleware)
- services.register(middlewares)
+ app.middleware.use(corsMiddleware)
En lugar de crear y registrar un MiddlewareConfig
para los servicios, el middleware ahora se expone como una propiedad de Application a la que se puede agregar.
// Realizar una solicitud en un controlador de ruta.
- try req.make(Client.self).get("https://vapor.codes")
+ req.client.get("https://vapor.codes")
Al igual que Application, Request también expone los servicios como propiedades y métodos simples. Los servicios específicos de Request siempre deben usarse cuando se encuentre dentro de un closure de ruta.
Este nuevo patrón de servicio reemplaza los tipos Container
, Service
y Config
de Vapor 3.
Providers¶
Ya no se requiere que los providers configuren paquetes de terceros. En cambio, cada paquete amplía Application y Request con nuevas propiedades y métodos de configuración.
Echa un vistazo a cómo está configurado Leaf en Vapor 4.
// Utilizar Leaf para renderizar vistas.
- try services.register(LeafProvider())
- config.prefer(LeafRenderer.self, for: ViewRenderer.self)
+ app.views.use(.leaf)
Para configurar Leaf, usa la propiedad app.leaf
.
// Deshabilitar el caché de vista de Leaf.
- services.register { container -> LeafConfig in
- return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
- }
+ app.leaf.cache.isEnabled = false
Entorno¶
Se puede acceder al entorno actual (producción, desarrollo, etc.) a través de app.environment
.
Servicios Personalizados¶
Los servicios personalizados que cumplen con el protocolo Service
y están registrados en el contenedor en Vapor 3 ahora se pueden expresar como extensiones de Application o Request.
struct MyAPI {
let client: Client
func foo() { ... }
}
- extension MyAPI: Service { }
- services.register { container -> MyAPI in
- return try MyAPI(client: container.make())
- }
+ extension Request {
+ var myAPI: MyAPI {
+ .init(client: self.client)
+ }
+ }
Luego se puede acceder a este servicio utilizando la extensión en lugar de make
.
- try req.make(MyAPI.self).foo()
+ req.myAPI.foo()
Providers Personalizados¶
La mayoría de los servicios personalizados se pueden implementar mediante extensiones como se muestra en la sección anterior. Sin embargo, es posible que algunos providers avanzados necesiten conectarse al ciclo de vida de la aplicación o utilizar propiedades almacenadas.
El nuevo helper Lifecycle
de la aplicación se puede utilizar para registrar controladores de ciclo de vida.
struct PrintHello: LifecycleHandler {
func willBoot(_ app: Application) throws {
print("Hello!")
}
}
app.lifecycle.use(PrintHello())
Para almacenar valores en Application, utilice el nuevo helper Storage
.
struct MyNumber: StorageKey {
typealias Value = Int
}
app.storage[MyNumber.self] = 5
print(app.storage[MyNumber.self]) // 5
El acceso a app.storage
se puede incluir en una propiedad calculada configurable para crear una API concisa.
extension Application {
var myNumber: Int? {
get { self.storage[MyNumber.self] }
set { self.storage[MyNumber.self] = newValue }
}
}
app.myNumber = 42
print(app.myNumber) // 42
NIO¶
Vapor 4 ahora expone las APIs asíncronas de SwiftNIO directamente y no intenta sobrecargar métodos como map
yflatMap
o tipos de alias como EventLoopFuture
. Vapor 3 proporcionó sobrecargas y alias para compatibilidad con versiones beta anteriores que se lanzaron antes de que existiera SwiftNIO. Estos se han eliminado para reducir la confusión con otros paquetes compatibles con SwiftNIO y seguir las recomendaciones de mejores prácticas de SwiftNIO.
Cambios de nombres asíncronos¶
El cambio más obvio es la eliminación del alias de tipo Future
para EventLoopFuture
. Esto se puede solucionar con bastante facilidad buscando y reemplazando.
Además, NIO no admite las etiquetas to:
que agregó Vapor 3. Dada la inferencia de tipos mejorada de Swift 5.2, to:
ahora es menos necesario de todos modos.
- futureA.map(to: String.self) { ... }
+ futureA.map { ... }
Los métodos con el prefijo new
, como newPromise
, se han cambiado a make
para adaptarse mejor al estilo de Swift.
- let promise = eventLoop.newPromise(String.self)
+ let promise = eventLoop.makePromise(of: String.self)
catchMap
ya no está disponible, pero los métodos de NIO como mapError
yflatMapErrorThrowing
funcionarán en su lugar.
El método global flatMap
de Vapor 3 para combinar múltiples futuros ya no está disponible. Esto se puede reemplazar utilizando el método and
de NIO para combinar muchos futuros.
- flatMap(futureA, futureB) { a, b in
+ futureA.and(futureB).flatMap { (a, b) in
// Do something with a and b.
}
ByteBuffer¶
Muchos métodos y propiedades que anteriormente usaban Data
ahora usan ByteBuffer
de NIO. Este tipo es un tipo de almacenamiento de bytes más potente y eficaz. Puedes leer más sobre su API en la documentación de ByteBuffer de SwiftNIO.
Para convertir un ByteBuffer
nuevamente en Data
, usa:
Data(buffer.readableBytesView)
Throwing map / flatMap¶
El cambio más difícil es que map
y flatMap
ya no lanzan errores (throw). map
tiene una versión throw llamada (de manera algo confusa) flatMapThrowing
. Sin embargo, flatMap
no tiene contrapartida throw. Esto puede requerir que tengas que reestructurar algo de código asincrónico.
Los maps que no hacen throw deberían seguir funcionando bien.
// map que no devuelve throw.
futureA.map { a in
return b
}
Los maps que si hacen throw deben cambiarse de nombre a flatMapThrowing
.
- futureA.map { a in
+ futureA.flatMapThrowing { a in
if ... {
throw SomeError()
} else {
return b
}
}
Los flatMap que no hacen throw deberían seguir funcionando bien.
// flatMap que no devuelve throw.
futureA.flatMap { a in
return futureB
}
En lugar de generar un error dentro de un flatMap, devuelva un error futuro. Si el error se origina en otro método throw, el error puede detectarse utilizando do / catch y devolverse como futuro.
// Devolver un error atrapado como futuro.
futureA.flatMap { a in
do {
try doSomething()
return futureB
} catch {
return eventLoop.makeFailedFuture(error)
}
}
Las llamadas a métodos que devuelven throw también se pueden refactorizar en un flatMapThrowing
y encadenarlas usando tuplas.
// Método de throw refactorizado en flatMapThrowing con encadenamiento de tuplas.
futureA.flatMapThrowing { a in
try (a, doSomeThing())
}.flatMap { (a, result) in
// result es el valor de doShomething.
return futureB
}
Routing¶
Las rutas ahora se registran directamente en Application.
app.get("hello") { req in
return "Hello, world"
}
Esto significa que ya no necesitas registrar un router en los servicios. Simplemente pasa tu aplicación a tu método routes
y comienza a agregar rutas. Todos los métodos disponibles en RoutesBuilder
están disponibles en Application
.
Contenido Sincrónico¶
El contenido de la solicitud de decodificación ahora es sincrónico.
let payload = try req.content.decode(MyPayload.self)
print(payload) // MyPayload
Este comportamiento puede ser anulado por rutas de registro utilizando la estrategia de recopilación de body .stream
.
app.on(.POST, "streaming", body: .stream) { req in
// El body de la solicitud ahora es asíncrono.
req.body.collect().map { buffer in
HTTPStatus.ok
}
}
Rutas separadas por comas¶
Las rutas ahora deben estar separadas por comas y no contener /
para mantener la coherencia.
- router.get("v1/users/", "posts", "/comments") { req in
+ app.get("v1", "users", "posts", "comments") { req in
// Handle request.
}
Parámetros de ruta¶
El protocolo Parameter
se ha eliminado en favor de parámetros nombrados explícitamente. Esto evita problemas con parámetros duplicados y recuperación desordenada de parámetros en middleware y controladores de ruta.
- router.get("planets", String.parameter) { req in
- let id = req.parameters.next(String.self)
+ app.get("planets", ":id") { req in
+ let id = req.parameters.get("id")
return "Planet id: \(id)"
}
El uso de parámetros de ruta con modelos se menciona en la sección de Fluent.
Middleware¶
MiddlewareConfig
pasó a llamarse MiddlewareConfiguration
y ahora es una propiedad de Application. Puedes agregar middleware a tu aplicación usando app.middleware
.
let corsMiddleware = CORSMiddleware(configuration: ...)
- var middleware = MiddlewareConfig()
- middleware.use(corsMiddleware)
+ app.middleware.use(corsMiddleware)
- services.register(middlewares)
El middleware ya no se puede registrar por nombre de tipo. Primero debes inicializar el middleware antes de registrarlo.
- middleware.use(ErrorMiddleware.self)
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))
Para eliminar todo el middleware predeterminado, establece app.middleware
en una configuración vacía usando:
app.middleware = .init()
Fluent¶
La API de Fluent ahora es independiente de la base de datos. Puedes importar solo Fluent
.
- import FluentMySQL
+ import Fluent
Modelos¶
Todos los modelos ahora usan el protocolo Model
y deben ser clases.
- struct Planet: MySQLModel {
+ final class Planet: Model {
Todos los campos se declaran utilizando los property wrappers @Field
o @OptionalField
.
+ @Field(key: "name")
var name: String
+ @OptionalField(key: "age")
var age: Int?
El ID de un modelo debe definirse utilizando el property wrapper @ID
.
+ @ID(key: .id)
var id: UUID?
Los modelos que usan un identificador con una clave o tipo personalizado deben usar @ID(custom:)
.
Todos los modelos deben tener su tabla o nombre de colección definido estáticamente.
final class Planet: Model {
+ static let schema = "Planet"
}
Todos los modelos ahora deben tener un inicializador vacío. Dado que todas las propiedades usan property wrappers, puede estar vacío.
final class Planet: Model {
+ init() { }
}
Los métodos save
, update
, y create
del modelo ya no devuelven la instancia del modelo.
- model.save(on: ...)
+ model.save(on: ...).map { model }
Los modelos ya no se pueden utilizar como componentes de ruta. Utiliza find
y req.parameters.get
en su lugar.
- try req.parameters.next(ServerSize.self)
+ ServerSize.find(req.parameters.get("size"), on: req.db)
+ .unwrap(or: Abort(.notFound))
Model.ID
ha sido renombrado a Model.IDValue
.
Los timestamps del modelo ahora se declaran usando el property wrapper @Timestamp
.
- static var createdAtKey: TimestampKey? = \.createdAt
+ @Timestamp(key: "createdAt", on: .create)
var createdAt: Date?
Relaciones¶
Las relaciones ahora se definen mediante property wrappers.
Las relaciones padres utilizan el property wrapper @Parent
y contienen la propiedad del campo internamente. La clave pasada a @Parent
debe ser el nombre del campo que almacena el identificador en la base de datos.
- var serverID: Int
- var server: Parent<App, Server> {
- parent(\.serverID)
- }
+ @Parent(key: "serverID")
+ var server: Server
Las relaciones hijas utilizan el property wrapper @Children
con una ruta clave al @Parent
relacionado.
- var apps: Children<Server, App> {
- children(\.serverID)
- }
+ @Children(for: \.$server)
+ var apps: [App]
Las relaciones entre hermanos utilizan el property wrapper @Siblings
con rutas clave al modelo de pivote.
- var users: Siblings<Company, User, Permission> {
- siblings()
- }
+ @Siblings(through: Permission.self, from: \.$user, to: \.$company)
+ var companies: [Company]
Los pivotes ahora son modelos normales que se ajustan a Model
con dos relaciones @Parent
y cero o más campos adicionales.
Consultas¶
Ahora se accede al contexto de la base de datos a través de req.db
en los controladores de ruta.
- Planet.query(on: req)
+ Planet.query(on: req.db)
Se ha cambiado el nombre de DatabaseConnectable
a Database
.
Las rutas clave a los campos ahora tienen el prefijo $
para especificar el contenedor de propiedad en lugar del valor del campo.
- filter(\.foo == ...)
+ filter(\.$foo == ...)
Migraciones¶
Los modelos ya no admiten migraciones automáticas basadas en reflection. Todas las migraciones deben escribirse manualmente.
- extension Planet: Migration { }
+ struct CreatePlanet: Migration {
+ ...
+}
Las migraciones ahora se escriben de forma estricta, están desacopladas de los modelos y utilizan el protocolo Migration
.
- struct CreateGalaxy: <#Database#>Migration {
+ struct CreateGalaxy: Migration {
Los métodos prepare
y revert
ya no son estáticos.
- static func prepare(on conn: <#Database#>Connection) -> Future<Void> {
+ func prepare(on database: Database) -> EventLoopFuture<Void>
La creación de un schema builder se realiza mediante un método de instancia Database
.
- <#Database#>Database.create(Galaxy.self, on: conn) { builder in
- // Use builder.
- }
+ var builder = database.schema("Galaxy")
+ // Use builder.
Los métodos create
, update
, y delete
ahora se llaman en el schema builder de manera similar a como funciona el query builder.
Las definiciones de los campos ahora están escritas en formato de cadena y siguen el siguiente patrón:
field(<name>, <type>, <constraints>)
Mira el ejemplo a continuación.
- builder.field(for: \.name)
+ builder.field("name", .string, .required)
El schema building ahora se puede encadenar como un query builder.
database.schema("Galaxy")
.id()
.field("name", .string, .required)
.create()
Configuración de Fluent¶
DatabasesConfig
ha sido reemplazado por app.databases
.
try app.databases.use(.postgres(url: "postgres://..."), as: .psql)
MigrationsConfig
ha sido reemplazado por app.migrations
.
app.migrations.use(CreatePlanet(), on: .psql)
Repositorios¶
Como la forma en que funcionan los servicios en Vapor 4 ha cambiado, la manera de hacer repositorios de bases de datos también. Aún necesitas un protocolo como UserRepository
, pero en lugar de hacer que una final class
se ajuste a ese protocolo, debes crear una struct
.
- final class DatabaseUserRepository: UserRepository {
+ struct DatabaseUserRepository: UserRepository {
let database: Database
func all() -> EventLoopFuture<[User]> {
return User.query(on: database).all()
}
}
También debes eliminar la conformidad de ServiceType
, puesto que ya no existe en Vapor 4.
- extension DatabaseUserRepository {
- static let serviceSupports: [Any.Type] = [Athlete.self]
- static func makeService(for worker: Container) throws -> Self {
- return .init()
- }
- }
En su lugar, deberías crear un UserRepositoryFactory
:
struct UserRepositoryFactory {
var make: ((Request) -> UserRepository)?
mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
self.make = make
}
}
Esta factory es responsable de devolver un UserRepository
para un Request
.
El siguiente paso es agregar una extensión a Application
para especificar su factory:
extension Application {
private struct UserRepositoryKey: StorageKey {
typealias Value = UserRepositoryFactory
}
var users: UserRepositoryFactory {
get {
self.storage[UserRepositoryKey.self] ?? .init()
}
set {
self.storage[UserRepositoryKey.self] = newValue
}
}
}
Para usar el repositorio creado dentro de una Request
, agrega esta extensión a la Request
:
extension Request {
var users: UserRepository {
self.application.users.make!(self)
}
}
El último paso es especificar el factory dentro de configure.swift
app.users.use { req in
DatabaseUserRepository(database: req.db)
}
Ahora puedes acceder a tu repositorio en tus controladores de ruta con: req.users.all()
y reemplazar fácilmente los tests internos del factory.
Si deseas utilizar un repositorio simulado dentro de los tests, primero crea un TestUserRepository
final class TestUserRepository: UserRepository {
var users: [User]
let eventLoop: EventLoop
init(users: [User] = [], eventLoop: EventLoop) {
self.users = users
self.eventLoop = eventLoop
}
func all() -> EventLoopFuture<[User]> {
eventLoop.makeSuccededFuture(self.users)
}
}
Ahora puedes usar este repositorio simulado dentro de tus tests de la siguiente manera:
final class MyTests: XCTestCase {
func test() throws {
let users: [User] = []
app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) }
...
}
}