How can I write multiple await queries to MongoDb in Rescript?

Help would be appreciated. I need to…

  1. Open a connection to MongoDB (done?)
  2. run multiple queries (find(), insertOne({ some record}, … )

In JS I would do initialize the connection to MongoDB in the app.js and then perform multiple await database queries in a try catch.

try {
const {_id as userId} = await /* ... */ 
await insertOne({ friend: userIdFromPreviousQuery })
} catch (e) { /* ... some error handling */}

Thanks to @kevanstannard I now have connected to the DB with the new Promises.

module Collection = {  
  type t  
  type queryResponse  
  
  @send external insertOne: t => /* object */ Promise.t<queryResponse> = "insertOne"  
  @send external findAll: t => /* empty */ Promise.t<queryResponse> = "find"  
}  
  
module Db = {  
  type t  
  
  @send external collection: (t, string) => Collection.t = "collection" 
}                   
                    
module ClientInstance = {  
  type t            
                    
  @send external db: (t, string) => Db.t = "db" 
}                   
                    
module Client = {   
  type t            
                    
  @module("mongodb") @new external make: string => t = "MongoClient"  
  @send external connect: t => Promise.t<ClientInstance.t> = "connect"  
}  
  
let uri = "mongodb://localhost:27017"  
let dbName = "Admin"  
let collectionName = "documents"  
  
open Promise  
  
Client.make(uri)  
->Client.connect  
->then(res => {  
  Js.log("The dbName is :" ++ dbName)
  let db = res->ClientInstance.db(dbName)           
  let collection = db->Db.collection(collectionName)  
  Js.log(collection)->resolve                       
})                                                  
->catch(e => {                                      
  switch e {                     
  | JsError(obj) =>                
    switch Js.Exn.message(obj) {                      
    | Some(msg) => Js.log("Some JS error" ++ msg)                              
    | None => Js.log("Unknown Js error")                                                
    }                            
  | _ => Js.log("Some unknown ReScript? error?")                
  }->resolve                                                                                             
})                                                           
->ignore    

But how do you perform multiple await queries, where the result from one can be used in the other?

Query 1:

const insertResult = await collection.insertMany([{ a: 1 }, { a: 2 }, { a: 3 }])
console.log('Inserted documents =>', insertResult)

Query 2:

const findResult = await collection.find({}).toArray()
console.log('Found documents =>', findResult)

What is the best workflow/practice to achieve this?

Same as in JavaScript with promises, you would use the promise ‘then’ method. The name depends on which promise library you’re using. But if you were using ReScript’s built-in one it would be Js.Promise.then_. So e.g.

// Query 1:
Collection.insertMany(collection, [{a: 1}, {a: 2}, {a: 3}])
|> Js.Promise.then_(insertResult =>
  Js.Promise.resolve(Js.log2("Inserted documents =>", insertResult)))

// Query 2:
Collection.find(collection, {...})
|> Collection.toArray
|> Js.Promise.then_(findResult =>
  Js.Promise.resolve(Js.log2("Found documents =>", findResult)))

Thanks @yawaramin . What I actually meant is how do I connect two promises correctly? The first promise is actually setting up the connection to MongoDB. And then there is a promise to do an actual command, like in this case the insertOne(). The second one cannot exist without the first one.

module Response = {  
  type t  
}  
  
module Collection = {  
  type t  
  @send external insertOne: (t, 'data) => Response.t = "insertOne"  
// This should probably be Promise.t<Response.t> instead ?
}  
  
module Db = {  
  type t  
  @send external collection: (t, string) => Collection.t = "collection"  
}  
  
module ClientInstance = {  
  type t  
  @send external db: (t, string) => Db.t = "db"  
}  
  
module Client = {  
  type t  
  @module("mongodb") @new external make: string => t = "MongoClient"  
  @send external connect: t => Promise.t<ClientInstance.t> = "connect"                                    
}                                                                                                         
                                                                                                          
let uri = "mongodb://localhost:27017"                                                                     
let dbName = "rescript"                                                                                   
let collectionName = "documents"                                                                          
type documentsType = {firstName: string, lastName: string, isLearningRescript: bool}  
let testDocument = {firstName: "Alex", lastName: "Inco", isLearningRescript: true}                        
                                                                                                          
open Promise                                                                                              
let client = Client.make(uri)                                                                             
client                                                                                                    
->Client.connect                                                                                          
->then(res => {                                                                                           
  let db = res->ClientInstance.db(dbName)                                                                 
  let collection = db->Db.collection(collectionName)                                                      
  let result = collection->Collection.insertOne(testDocument)                                             
  Js.log(result)->resolve                                                                                 
})                                                                                                        
->ignore                                                                                                  
~                

I can’t really call the Collection without first setting up the new MongoDBClient and then setting the dbName and collectionName first. Right?

So that would mean I would have to call the insertOne promise inside the resolved promise of the connection of the MongoClient. And that is considered incorrect based on @ryyppy documentation of rescript-promise.

I need to do two things…

  1. Set up a connection to MongoDB (which is a promise)
  2. Then use that connection to perform actions like find, insertOne, … on that connection. But these actions also are promises.

:frowning: That’s where I am stuck.

You can return a promise, and even the result of another promise chain within a then body. What you are not supposed to do is call Promise.resolve on a promise value, so that it forwards a Promise.t<Promise.t<...>> to the next then block.

Here’s a quick draft for your full example that show-cases the promise chaining:

module Response = {
  type t

  @send external toArray: t => array<'a> = "toArray"
}

module Collection = {
  type t
  @send external insertOne: (t, 'data) => Promise.t<Response.t> = "insertOne"
  @send external find: 'query => Promise.t<Response.t> = "find"
}

module Db = {
  type t
  @send external collection: (t, string) => Collection.t = "collection"
}

module ClientInstance = {
  type t
  @send external db: (t, string) => Db.t = "db"
}

module Client = {
  type t
  @module("mongodb") @new external make: string => t = "MongoClient"
  @send external connect: t => Promise.t<ClientInstance.t> = "connect"
  @send external close: t => unit = "close"
}

let uri = "mongodb://localhost:27017"
let dbName = "rescript"
let collectionName = "documents"
type documentsType = {firstName: string, lastName: string, isLearningRescript: bool}
let testDocument = {firstName: "Alex", lastName: "Inco", isLearningRescript: true}

// Inserts some fixture test document and then finds the inserted data afterwards
let insertAndShow = (collection: Collection.t): Promise.t<array<documentsType>> => {
  collection
  ->Collection.insertOne(testDocument)
  ->Promise.then(result => {
    Js.log2("insertResult", result)
    Collection.find(Js.Obj.empty())
  })
  ->Promise.thenResolve(findResult => {
    findResult->Response.toArray
  })
}

open Promise
let client = Client.make(uri)
client
->Client.connect
->then(res => {
  let db = res->ClientInstance.db(dbName)
  let collection = db->Db.collection(collectionName)

  insertAndShow(collection)
})
->thenResolve(result => {
  Js.log2("find result", result)
})
->Promise.finally(() => {
  client->Client.close
})
->ignore

2 Likes

Thanks @ryyppy. Now I want to also catch errors. When I add the catch block to the mix, I get the error that there is a mismatch in types between the then and the catch block.

open Promise                     
                                 
let client = Client.make(uri)                                                                             
client                           
->Client.connect                 
->then(clientInstance => {                                                                                
  let db = clientInstance->ClientInstance.db(dbName)
  let collection = db->Db.collection(collectionName)
  collection                      
  ->Collection.insertOne({firstName: "Alex", lastName: "Incognito", isLearningRescript: true})            
  ->resolve                               
})                                        
->catch(e => {                            
  switch e {                              
  | JsError(obj) =>                                                             
    switch Js.Exn.message(obj) {                                                
    | Some(msg) => Js.log("Some JS error: " ++ msg)                             
    | None => Js.log("Some non JS error")                                       
    }                                                                           
  | _ => Js.log("An unknown error")                                             
  }->resolve                                                                    
})                                                                                                        
->then(response => {                                                            
  Js.log(response)                                                              
})                                                                              
->finally(() => {                                                               
  client->Client.close                                                          
})   

I get this error:

[ReScript] [E] This has type: Promise.t<unit> (defined as Js_promise.t<unit>) 
  Somewhere wanted:                                                             
    Promise.t<Promise.t<Result.t>> (defined as                                  
      Js_promise.t<Promise.t<Result.t>>)                                        
                                                                                
} The incompatible parts:                                                                                 
-   unit vs Promise.t<Result.t> (defined as Js_promise.t<Result.t>)      

I understand I have to change the Response type, to accommodate not only the response from the then block but also from the catch block.

I tried changing the type of the insertOne as follows:

@send external insertOne: (t, 'data) => Promise.t<(Result.t, Js.Nullable.t<Js.Exn.t>)> = "insertOne"

But that didn’t work.

How to fix this?

I believe you’ll need to ensure your then() and catch() functions return the same types. To keep things simple, you might like to have them each return a Promise.t<unit>.

Here’s a tweaked version of your code:

open Promise

let client = Client.make(uri)
client
->Client.connect
->then(clientInstance => {
  let db = clientInstance->ClientInstance.db(dbName)
  let collection = db->Db.collection(collectionName)
  // Do the insert, then transform the Promise.t<Response.t> into Promise.t<unit>
  collection
  ->Collection.insertOne({firstName: "Alex", lastName: "Incognito", isLearningRescript: true})
  ->then(_ => resolve())
})
->catch(e => {
  switch e {
  | JsError(obj) =>
    switch Js.Exn.message(obj) {
    | Some(msg) => Js.log("Some JS error: " ++ msg)
    | None => Js.log("Some non JS error")
    }
  | _ => Js.log("An unknown error")
  }
  // Return a Promise.t<unit>
  resolve()
})
->then(response => {
  Js.log(response)
  // Return a Promise.t<unit>
  resolve()
})
->finally(() => {
  client->Client.close
})
// Ignore the top level returned Promise.t<unit>
->ignore
2 Likes