Search code examples
swiftasync-awaitvaporvapor-fluentswift5.6

Creating CRUD Functions Using Async-Await with Vapor-Fluent - Swift 5.6


I'm trying to understand how to create CRUD functions with Vapor. It appears first() is optional, but cannot be unwrapped as an optional type

func first() -> EventLoopFuture<Token?>

Here's my Token.swift

import Foundation
import Fluent
import Vapor

final class Token: Model {
  // 1
  static let schema = "tokens"

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

  // 3
  @Field(key: "token")
  var token: String

  @Field(key: "debug")
  var debug: Bool

  // 4
  init() { }

  init(token: String, debug: Bool) {
    self.token = token
    self.debug = debug
  }
}

Here's my TokenController.swift

import Foundation
import Fluent
import Vapor

struct TokenController {
    func create(req: Request) async throws -> HTTPStatus {
        let token = try req.content.decode(Token.self)
        try await token.create(on: req.db)
        return .created
    }
    
    func delete(req: Request) async throws -> HTTPStatus {
        guard let token = req.parameters.get("token") else {
            return .badRequest
        }
        
        try await Token.query(on: req.db)
            .filter(\.$token == token)
            .first()
        
        try await delete(req: req.db)

        return .noContent
    }

}

@available(macOS 12, *)
extension TokenController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let tokens = routes.grouped("token")
        tokens.post(use: create)
        tokens.delete(":token", use: delete)
    }
}

The return token might not be there, so it should be optional, but I get the familiar error

Initializer for conditional binding must have Optional type, not 'EventLoopFuture<Token?>'

I'm using async-await obviously and not EventLoopFuture<> except maybe under the hood, if I understand the documentation correctly.

Based on a tutorial I'm following, this was suggested as a solution to delete rows by getting the first row in the table

func delete(req: Request) async throws -> HTTPStatus {
  guard let token = req.parameters.get("token") else {
    return .badRequest
  }

  guard let row = try await Token.query(on: req.db)
    .filter(\.$token == token)
    .first()
  else {
    return .notFound
  }

  try await row.delete(on: req.db)
  return .noContent
}

But this gives me the same error for token, including another error for the row.delete(on:_) method

Value of type 'EventLoopFuture<Token?>' has no member 'delete'

What am I missing?

• Pouring over the documentation • Retracing my steps from the tutorial • Holding my breath until the computer worked


Solution

  • You probably need to give the compiler a hand and force it to use the async version of .first():

    guard let row: Token = try await Token.query(on: req.db)
        .filter(\.$token == token)
        .first()
    

    Because the functions have the same signature it can occasionally pick the future version, especially if there are errors elsewhere