Vapor 3 - How to Check for Similar Email Before Saving Object

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

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.

Vapor How to find user by email

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 decoding content: do/catch for multiple post formats?

Figured it out!

The decode method returns a Future, such that the actual decoding (and hence the error) occurs later, not during the do/catch. This means there's no way to catch the error with this do catch.

Luckily, Futures have a series of methods prepended with catch; the one I'm interested in is catchFlatMap, which accepts a closure from Error -> Future<Decodable>. This method 'catches' the errors thrown in the called Future, and passes the error to the closure, using the result in any downstream futures.

So I was able to change my code to:

func createHandler(_ req: Request) throws -> Future<Widget> {
return try req.content.decode(WidgetCreateHolder.self).catchFlatMap({ _ in
return try req.content.decode(WidgetCreateObject.self).map(to: WidgetCreateHolder.self) {
return WidgetCreateHolder(widget: $0)
}
}).flatMap(to: Widget.self) {
return createWidget(from: $0.widget)
}
}

How to read a parameter in a Vapor middleware without consuming it

Heeey, it looks like you could get your Campaign from request without dropping it like this

guard let parameter = req.parameters.values.first else {
throw Abort(.forbidden)
}
try Campaign.resolveParameter(parameter.value, on: req)

So your final code may look like

final class CampaignMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
let user = try request.requireAuthenticated(User.self)
guard let parameter = request.parameters.values.first else {
throw Abort(.forbidden)
}
return try Campaign.resolveParameter(parameter.value, on: request).flatMap { campaign in
guard try campaign.userID == user.requireID() else {
throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
}
return try next.respond(to: request)
}
}
}

Where do I get the database object using Vapor?

I'm not sure how I missed it before, but it is accessible with

app.db

Vapor 3: transform array of Future object to an array of Future other objects

The logic you're looking for will look like this

extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
return try CourseUser.query(on: req)
.filter(\.courseID == self.requireID())
.all().flatMap { courseUsers in
// here we should query a user for each courseUser
// and only then convert all of them into PublicCourseData
// but it will execute a lot of queries and it's not a good idea
}
}
}
}

I suggest you to use the SwifQL lib instead to build a custom query to get needed data in one request /p>

You could mix Fluent's queries with SwifQL's in case if you want to get only one course, so you'll get it in 2 requests:

struct Student: Content {
let name: String
let progress: Int
}

extension Course {
func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
return teacher.get(on: req).flatMap { teacher in
// we could use SwifQL here to query students in one request
return SwifQL.select(\CourseUser.progress, \User.name)
.from(CourseUser.table)
.join(.inner, User.table, on: \CourseUser.userID == \User.id)
.execute(on: req, as: .psql)
.all(decoding: Student.self).map { students in
return try PublicCourseData(id: self.requireID(),
name: self.name,
teacher: teacher,
students: students)
}
}
}
}

If you want to get a list of courses in one request you could use pure SwifQL query.

I simplified desired JSON a little bit

{
"id": 1,
"name": "Course 1",
"teacher": {"name": "Mr. Teacher"},
"students": [
{"name": "Student 1", progress: 10},
{"name": "Student 2", progress: 60},
]
}

first of all let's create a model to be able to decode query result into it

struct CoursePublic: Content {
let id: Int
let name: String
struct Teacher:: Codable {
let name: String
}
let teacher: Teacher
struct Student:: Codable {
let name: String
let progress: Int
}
let students: [Student]
}

Ok now we are ready to build a custom query. Let's build it in some request handler function

func getCourses(_ req: Request) throws -> Future<[CoursePublic]> {
/// create an alias for student
let s = User.as("student")

/// build a PostgreSQL's json object for student
let studentObject = PgJsonObject()
.field(key: "name", value: s~\.name)
.field(key: "progress", value: \CourseUser.progress)

/// Build students subquery
let studentsSubQuery = SwifQL
.select(Fn.coalesce(Fn.jsonb_agg(studentObject),
PgArray(emptyMode: .dollar) => .jsonb))
.from(s.table)
.where(s~\.id == \CourseUser.userID)

/// Finally build the whole query
let query = SwifQLSelectBuilder()
.select(\Course.id, \Course.name)
.select(Fn.to_jsonb(User.table) => "teacher")
.select(|studentsSubQuery| => "students")
.from(User.table)
.join(.inner, User.table, on: \Course.teacherId == \User.id)
.join(.leftOuter, CourseUser.table, on: \CourseUser.teacherId == \User.id)
.build()
/// this way you could print raw query
/// to execute it in postgres manually
/// for debugging purposes (e.g. in Postico app)
print("raw query: " + query.prepare(.psql).plain)
/// executes query with postgres dialect
return query.execute(on: req, as: .psql)
/// requests an array of results (or use .first if you need only one first row)
/// You also could decode query results into the custom struct
.all(decoding: CoursePublic.self)
}

Hope it will help you. There may be some mistakes in the query cause I wrote it without checking You can try to print a raw query to copy it and execute in e.g. Postico app in postgres directly to understand what's wrong.



Related Topics



Leave a reply



Submit