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
.
中创建响应
我想创建一个路由让用户更新他们的数据(例如更改他们的电子邮件或用户名)。为了确保一个用户不能使用与另一个用户相同的用户名,我想检查数据库中是否已经存在具有相同用户名的用户。
我已经在迁移中设置了唯一的用户名。
我有一个如下所示的用户模型:
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
.