Vapor 4 fluent. Complex query, filter and create if doesn't exist

Viewed 1660

I'm trying to query a Chat with two users. If it exists I'd like to return it. If not I'd like to create new chat with those two users and return it.

Comment shows where it fails if chat exists.

Also would be helpful if you can show how to create new chat with those two users in this scenario.

func create(req: Request) throws -> EventLoopFuture<Chat> {
  
  let currentUser = try req.auth.require(User.self)
  
  guard
    let userID = req.parameters.get("userID"),
    let uuid = UUID(userID)
  else { throw Abort(.badRequest) }
  
  return User
    .query(on: req.db)
    .filter(\.$id == uuid)
    .with(\.$chats)
    .first()
    .unwrap(or: Abort(.internalServerError))
    .flatMap({ chatUser -> EventLoopFuture<Chat> in
      
      let chat = chatUser
        .chats
        .filter({
          $0.users.contains(where: { $0.id == currentUser.id }) // FAILS HERE: Fatal error: Siblings relation not eager loaded, use $ prefix to access: Siblings<Chat, User, ChatUser>(from: [chat_id], to: [user_id]): file
        })
        .first
      
      if let chat = chat {
        
        return req
          .eventLoop
          .makeSucceededFuture(chat)
        
      } else {
        
        // create new chat with those two users and return
        
      }
    })
}

Simplified models:

final class Chat: Model, Content {
  @ID(key: .id)
  var id: UUID?
  
  @Siblings(through: ChatUser.self, from: \.$chat, to: \.$user)
  var users: [User]
}

final class User: Model, Content {
  @ID(key: .id)
  var id: UUID?
  
  @Siblings(through: ChatUser.self, from: \.$user, to: \.$chat)
  var chats: [Chat]
}

final class ChatUser: Model {
  @ID(key: .id)
  var id: UUID?

  @Parent(key: Keys.chatId)
  var chat: Chat

  @Parent(key: Keys.userId)
  var user: User
}
2 Answers

0xTim helped me out on Discord. And I managed to make it work after reading This doc

Here's working solution:

func create(req: Request) throws -> EventLoopFuture<Chat> {
  
  let currentUser = try req.auth.require(User.self)
  
  guard
    let userID = req.parameters.get("userID"),
    let userUUID = UUID(userID)
    else { throw Abort(.badRequest) }
  
  return User
    .query(on: req.db)
    .filter(\.$id == userUUID)
    .with(\.$chats, { chats in
      chats.with(\.$users)
    })
    .first()
    .unwrap(or: Abort(.internalServerError))
    .flatMap({ chatUser -> EventLoopFuture<Chat> in
      
      let chat = chatUser
        .chats
        .filter({
          $0.users.contains(where: { $0.id! == currentUser.id! })
        })
        .first
      
      if let chat = chat {
        
        return req.eventLoop.makeSucceededFuture(chat)
        
      } else {
        
        let newChat = Chat()
        return newChat
          .save(on: req.db)
          .flatMap({
            
            newChat
              .$users
              .attach([chatUser, currentUser], on: req.db)
              .flatMap({
                
                return req.eventLoop.makeSucceededFuture(newChat)
              })
          })
      }
    })
}

Your problem is that essentially you need to do the further eager load of your User model being used in the ChatUser model. If it were possible, in your first query, as well as .with(\.$chats) you would do something like .with(\.$chats.$chats), but this isn't possible.

In this case, rather than answer your question directly, could I suggest that you think about starting with a query on ChatUser instead? If you look for a record in this Model that matches and then work accordingly. Something like:

ChatUser.query(on:req.db).filter(\.$id == currentUser.id).filter(\.$chat == userId).first().map { mayExist in
    if let doesExist = mayExist {
        // work with existing chat
    } else {
        // does not exist so create a new one
        // how, depends on your init for the model
        let newChat = ChatUser( user:currentUser.id!, chat:userId)
        newChat.create(on:req.db).map{
            // do something here
        }
    }
}

PS - in a rush to go to work, so this may be full of holes!

Related