Vapor 3 - 如何在保存对象之前检查类似的电子邮件

Vapor 3 - How to check for similar email before saving object

我想创建一个路由让用户更新他们的数据(例如更改他们的电子邮件或用户名)。为了确保一个用户不能使用与另一个用户相同的用户名,我想检查数据库中是否已经存在具有相同用户名的用户。

我已经在迁移中设置了唯一的用户名。

我有一个如下所示的用户模型:

struct User: Content, SQLiteModel, Migration {
    var id: Int?
    var username: String
    var name: String
    var email: String
    var password: String

    var creationDate: Date?

    // Permissions
    var staff: Bool = false
    var superuser: Bool = false

    init(username: String, name: String, email: String, password: String) {
        self.username = username
        self.name = name
        self.email = email
        self.password = password
        self.creationDate = Date()
    }
}

这是我要使用它的代码片段:

func create(_ req: Request) throws -> EventLoopFuture<User> {
    return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in

        // Check if `userRequest.email` already exists
        // If if does -> throw Abort(.badRequest, reason: "Email already in use")
        // Else -> Go on with creation

        let digest = try req.make(BCryptDigest.self)
        let hashedPassword = try digest.hash(userRequest.password)
        let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)

        return persistedUser.save(on: req)
    }
}

我可以这样做(见下一个片段)但它似乎是一个奇怪的选择,因为它需要大量的嵌套,例如更多的检查。必须执行唯一性(例如在更新用户的情况下)。

func create(_ req: Request) throws -> EventLoopFuture<User> {
    return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
        let userID = userRequest.email
        return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
            guard existingUser == nil else {
                throw Abort(.badRequest, reason: "A user with this email already exists")
            }

            let digest = try req.make(BCryptDigest.self)
            let hashedPassword = try digest.hash(userRequest.password)
            let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)

            return persistedUser.save(on: req)
        }
    }
}

作为建议的答案之一,我尝试添加错误中间件(请参阅下一个片段),但这并没有正确捕获错误(也许我在代码中做错了什么 - 刚开始使用 Vapor)。

import Vapor
import FluentSQLite

enum InternalError: Error {
    case emailDuplicate
}

struct EmailDuplicateErrorMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
        let response: Future<Response>

        do {
            response = try next.respond(to: request)
        } catch is SQLiteError {
            response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
        }

        return response.catchFlatMap { error in
            if let response = error as? ResponseEncodable {
                do {
                    return try response.encode(for: request)
                } catch {
                    return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
                }
            } else {
                return request.eventLoop.newFailedFuture(error: error)
            }
        }
    }
}

快速的方法是执行类似 User.query(on: req).filter(\.email == email).count() 的操作,并在尝试保存之前检查它是否等于 0。

然而,虽然这对几乎所有人都适用,但您仍然会遇到两个用户同时尝试使用相同用户名注册的边缘情况 - 处理此问题的唯一方法是捕获保存失败,检查是否是因为电子邮件的唯一约束和 return 给用户的错误。然而,即使是大型应用程序,您真正达到目标的机会也很少。

我会使用 Migration 在模型中创建字段 unique,例如:

extension User: Migration {
  static func prepare(on connection: SQLiteConnection) -> Future<Void> {
    return Database.create(self, on: connection) { builder in
      try addProperties(to: builder)
      builder.unique(on: \.email)
    }
  }
}

如果您使用默认 String 作为 email 的字段类型,那么您将需要减少它,因为这会创建一个字段 VARCHAR(255),这对于 UNIQUE键。然后我会使用一些自定义 Middleware 来捕获在使用同一电子邮件第二次尝试保存记录时出现的错误。

struct DupEmailErrorMiddleware: Middleware
{
    func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
    {
        let response: Future<Response>
        do {
            response = try next.respond(to: request)
        } catch is MySQLError {
            // needs a bit more sophistication to check the specific error
            response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
        }
        return response.catchFlatMap
        {
            error in
            if let response = error as? ResponseEncodable
            {
                do
                {
                    return try response.encode(for: request)
                }
                catch
                {
                    return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
                }
            } else
            {
                return request.eventLoop.newFailedFuture(error: error   )
            }
        }
    }
}

编辑:

您的自定义错误需要类似于:

enum InternalError: Debuggable, ResponseEncodable
{
    func encode(for request: Request) throws -> EventLoopFuture<Response>
    {
        let response = request.response()
        let eventController = EventController()
        //TODO make this return to correct view
        eventController.message = reason
        return try eventController.index(request).map
        {
            html in
            try response.content.encode(html)
            return response
        }
    }

    case dupEmail

    var identifier:String
    {
        switch self
        {
            case .dupEmail: return "dupEmail"
        }
    }

    var reason:String
    {
       switch self
       {
            case .dupEmail: return "Email address already used"
        }
    }
}

在上面的代码中,通过在控制器中设置一个值向用户显示实际错误,然后在视图中拾取该值并显示警告。此方法允许通用错误处理程序负责显示错误消息。但是,在您的情况下,您可能只需在 catchFlatMap.

中创建响应