Chaining two futures in Swift Vapor framework - swift

I have this function that checks if an username already exists in the database during registration (REST API). If the username already exists, a nice error message is displayed. Now I want to add the same check for the email, with a nice error message and a check if both username and email already exists, again with it's own nice error message.
I don't have much experience with async coding and I don't understand how chain the two futures.
This is the main function:
fileprivate func create(req: Request) throws -> EventLoopFuture<NewSession> {
try UserSignup.validate(content: req)
let userSignup = try req.content.decode(UserSignup.self)
let user = try User.create(from: userSignup)
var token: Token!
return checkIfUserExists(userSignup.username, req: req).flatMap { exists in
guard !exists else {
return req.eventLoop.future(error: UserError.usernameTaken)
}
return user.save(on: req.db)
}.flatMap {
guard let newToken = try? user.createToken(source: .signup) else {
return req.eventLoop.future(error: Abort(.internalServerError))
}
token = newToken
return token.save(on: req.db)
}.flatMapThrowing {
NewSession(token: token.value, user: try user.asPublic())
}
}
This is the checkIfUserExists function:
private func checkIfUserExists(_ username: String, req: Request) -> EventLoopFuture<Bool> {
User.query(on: req.db)
.filter(\.$username == username)
.first()
.map { $0 != nil }
}
This is the checkIfEmailExists function:
private func checkIfEmailExists(_ email: String, req: Request) -> EventLoopFuture<Bool> {
User.query(on: req.db)
.filter(\.$email == email)
.first()
.map { $0 != nil }
}
I've tried if-else, tried .add() and other weird stuff but I can't get it to work. Also I need to keep this syntax and not using the async/await syntax.

Modify your query to include a group:
query.group(.or) { group in
group.filter(\User.$username == username).filter(\User.$email == email)
}

Related

Vapor how to transform EventLoopFuture<Type> to <Type>

I am implementing a delete route handler using Vapor Fluent.
For this handler, I wanted to verify that the user sending the product deletion request is the owner of the product, and Abort the request otherwise.
func deleteHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
let user = req.auth.get(User.self)
return Product.find(req.parameters.get("productID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { product in
return product.$user.get(on: req.db).flatMapThrowing { owner in
guard try user?.requireID() == owner.requireID() else {
throw Abort(.forbidden)
}
return try product.delete(on: req.db)
.transform(to: HTTPStatus.noContent) // error here
}
}
}
But Vapor throws an error at return try product.delete(on: req.db).transform(to: HTTPStatus.noContent) saying Cannot convert return expression of type 'EventLoopFuture<HTTPResponseStatus>' to return type 'HTTPStatus' (aka 'HTTPResponseStatus').
I tried chaining again using map({}), that did not help. Using wait()solves the error but introduces a runtime bug.
Thanks for any help!
The problem is that the .flatMapThrowing isn't throwing the future. requireID() is overkill in this context. If you replace the throw as follows and remove the Throwing as a result, it should work:
func deleteHandler(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
let user = req.auth.get(User.self)
return Product.find(req.parameters.get("productID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { product in
return product.$user.get(on: req.db).flatMap { owner in
guard user?.id == owner.id else {
return request.eventLoop.makeFailedFuture( Abort(.forbidden))
}
return product.delete(on: req.db)
.transform(to: HTTPStatus.noContent)
}
}
}
I've removed the try from the delete later on. I'm guessing the compiler didn't report this as unnecessary because of the earlier error, but it isn't usually needed. My answer will fail spectacularly if it is!

Vapor How to find user by email

How to properly find user by email in vapor in login method and return that user or return error,
I've tried:
func login(_ req: Request) throws -> Future<User> {
return try req.content.decode(User.self).map { loginUser in
let query = User.query(on: req)
return query
.filter(\.email == loginUser.email)
.first()
.flatMap { user in
return user!.save(on: req)
}
}
}
but I'm getting
Cannot convert return expression of type 'EventLoopFuture' to return type 'User'
func login(_ req: Request) throws -> Future<User> {
return try req.content.decode(User.self).flatMap { loginUser in
return User.query(on: req)
.filter(\.email == loginUser.email)
.first()
.unwrap(or: Abort(.notFound, reason: "User not found"))
}
}

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

I would like to create a route to let users update their data (e.g. changing their email or their username). To make sure a user cannot use the same username as another user, I would like to check if a user with the same username already exists in the database.
I have already made the username unique in the migrations.
I have a user model that looks like this:
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()
}
}
This is the piece of code where I want to use it:
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)
}
}
I could do it like this (see next snippet) but it seems a strange option as it requires a lot of nesting when more checks for e.g. uniqueness would have to be performed (for instance in the case of updating a user).
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)
}
}
}
As one of the answers suggested I've tried to add Error middleware (see next snippet) but this does not correctly catch the error (maybe I am doing something wrong in the code - just started with 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)
}
}
}
}
The quick way of doing it is to do something like User.query(on: req).filter(\.email == email).count() and check that equals 0 before attempting the save.
However, whilst this will work fine for almost everyone, you still risk edge cases where two users try to register with the same username at the exact same time - the only way to handle this is to catch the save failure, check if it was because the unique constraint on the email and return the error to the user. However the chances of you actually hitting that are pretty rare, even for big apps.
I would make the field unique in the model using a Migration such as:
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)
}
}
}
If you use a default String as the field type for email, then you will need to reduce it as this creates a field VARCHAR(255) which is too big for a UNIQUE key. I would then use a bit of custom Middleware to trap the error that arises when a second attempt to save a record is made using the same email.
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 )
}
}
}
}
EDIT:
Your custom error needs to be something like:
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"
}
}
}
In the code above, the actual error is displayed to the user by setting a value in the controller, which is then picked up in the view and an alert displayed. This method allows a general-purpose error handler to take care of displaying the error messages. However, in your case, it might be that you could just create the response in the catchFlatMap.

Call own function with a completion handler

I have a method to create an account:
func createAccount (completion: #escaping (_ succes: Bool, _ message : String)->()) {
Auth.auth().createUser(withEmail: createMail(), password: createPassword()) { (result, error) in
if let _eror = error {
//something bad happning
print(_eror.localizedDescription )
if let errorCode = AuthErrorCode(rawValue: _eror._code) {
if(errorCode.rawValue == 17007) {
print("acount exist")
createAccount(completion: (Bool, String) -> ()
} else {
//call itself and try it again
}
}
} else {
//user registered successfully
print("user registered")
return completion(true, "");
}
}
}
I get an error when the software creates an account with an email that already exists, which is good (see the else statement - //call itself and try it again).
What needs to happen is that the function needs to call itself again to try it with a different email.
I tried to put createAccount(completion: (Bool, String) -> () in the else case, but that didn't work.
How can I call the createAccount() function again in the else case?
You need to pass the same paramter again
createAccount(completion:completion)

Best practice for Swift methods that can return or error [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 1 year ago.
Improve this question
I’m practicing Swift and have a scenario (and a method) where the result could either be successful or a failure.
It’s a security service class. I have a method where I can authenticate with an email address and password, and want to either return a User instance if the credentials are correct, or throw some form of false value.
I’m a bit confused as my understanding of Swift methods is you need to specify a return type, so I have:
class SecurityService {
static func loginWith(email: String, password: String) -> User {
// Body
}
}
I’ve seen in Go and Node.js methods that return a “double” value where the first represents any errors, and the second is the “success” response. I also know that Swift doesn’t have things like errors or exceptions (but that may have changed since as I was learning an early version of Swift).
What would be the appropriate thing to do in this scenario?
If you want to handle errors that can happen during login process than use the power of Swift error handling:
struct User {
}
enum SecurityError: Error {
case emptyEmail
case emptyPassword
}
class SecurityService {
static func loginWith(email: String, password: String) throws -> User {
if email.isEmpty {
throw SecurityError.emptyEmail
}
if password.isEmpty {
throw SecurityError.emptyPassword
}
return User()
}
}
do {
let user = try SecurityService.loginWith1(email: "", password: "")
} catch SecurityError.emptyEmail {
// email is empty
} catch SecurityError.emptyPassword {
// password is empty
} catch {
print("\(error)")
}
Or convert to optional:
guard let user = try? SecurityService.loginWith(email: "", password: "") else {
// error during login, handle and return
return
}
// successful login, do something with `user`
If you just want to get User or nil:
class SecurityService {
static func loginWith(email: String, password: String) -> User? {
if !email.isEmpty && !password.isEmpty {
return User()
} else {
return nil
}
}
}
if let user = SecurityService.loginWith(email: "", password: "") {
// do something with user
} else {
// error
}
// or
guard let user = SecurityService.loginWith(email: "", password: "") else {
// error
return
}
// do something with user
Besides the standard way to throw errors you can use also an enum with associated types as return type
struct User {}
enum LoginResult {
case success(User)
case failure(String)
}
class SecurityService {
static func loginWith(email: String, password: String) -> LoginResult {
if email.isEmpty { return .failure("Email is empty") }
if password.isEmpty { return .failure("Password is empty") }
return .success(User())
}
}
And call it:
let result = SecurityService.loginWith("Foo", password: "Bar")
switch result {
case .Success(let user) :
print(user)
// do something with the user
case .Failure(let errormessage) :
print(errormessage)
// handle the error
}
Returning a result enum with associated values, throwing exception, and using a callback with optional error and optional user, although valid make an assumption of login failure being an error. However thats not necessarily always the case.
Returning an enum with cases for success and failure containing result and error respectively is almost identical as returning an optional User?. More like writing a custom optional enum, both end up cluttering the caller. In addition, it only works if the login process is synchronous.
Returning result through a callBack, looks better as it allows for the operation to be async. But there is still error handling right in front of callers face.
Throwing is generally preferred than returning an error as long as the scope of the caller is the right place to handle the error, or at least the caller has access to an object/method that can handle this error.
Here is an alternative:
func login(with login: Login, failure: ((LoginError) -> ())?, success: (User) -> ()?) {
if successful {
success?(user)
} else {
failure?(customError)
}
}
// Rename with exactly how this handles the error if you'd have more handlers,
// Document the existence of this handler, so caller can pass it along if they wish to.
func handleLoginError(_ error: LoginError) {
// Error handling
}
Now caller can; simply decide to ignore the error or pass a handler function/closure.
login(with: Login("email", "password"), failure: nil) { user in
// Ignores the error
}
login(with: Login("email", "password"), failure: handleLoginError) { user in
// Lets the error be handled by the "default" handler.
}
PS, Its a good idea to create a data structure for related fields; Login email and password, rather individually setting the properties.
struct Login {
typealias Email = String
typealias Password = String
let email: Email
let password: Password
}
To add an answer to this question (five years later), there’s a dedicated Result type for this exact scenario. It can return the type you want on success, or type an error on failure.
It does mean re-factoring some code to instead accept a completion handler, and then enumerating over the result in that callback:
class SecurityService {
static func loginWith(email: String, password: String, completionHandler: #escaping (Result<User, SecurityError>) -> Void) {
// Body
}
}
Then in a handler:
securityService.loginWith(email: email, password: password) { result in
switch result {
case .success(let user):
// Do something with user
print("Authenticated as \(user.name)")
case .failure(let error):
// Do something with error
print(error.localizedDescription)
}
}
I think that the result of calling loginWith could be derived from a network request, here is code that I could do in the scenario you presented:
Helper classes:
struct User {
var name: String
var email: String
}
class HTTP {
static func request(URL: String, method: String, params: [String: AnyObject], callback: (error: NSError?, result: [String:AnyObject]?) -> Void) -> Void {
// network request
}
}
class SecurityService {
static func loginWith(email: String, password: String, callback: (error: NSError?, user: User?) -> Void) -> Void {
let URL = ".."
let params = [
"email": email,
"password": password
]
HTTP.request(URL, method: "POST", params: params) { (error, result) in
if let error = error {
callback(error: error, user: nil)
} else {
guard let JSON = result else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No results were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
guard let name = JSON["name"] as? String, email = JSON["email"] as? String else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No user properties were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
let user = User(name: name, email: email)
callback(error: nil, user: user)
}
}
}
}
Using the SecurityService class:
SecurityService.loginWith("someone#email.com", password: "123456") { (error, user) in
if let error = error {
print(error)
} else {
guard let user = user else {
print("no user found.")
return
}
print(user)
}
}