Skip to content

Make the EventEmitter returned by query() thenable #1253

Open
@felixfbecker

Description

@felixfbecker

There have been some discussions about promise support, but those are very old. It is pretty clear that the future is moving towards promises / ES7 async-await.
Wrappers like promise-mysql have the problem of not always being up-to-date, hiding the EventEmitter by returning a promise and not implementing all APIs (like pools).

I would propose to add a then() method to the EventEmitter returned by query() and other async functions that take a callback, therefor implementing the promise interface.
The old API would not break in any way, but it would allow users to await a query.
It could also be beneficial to add the 2 other standard promise functions, catch() and finally(), otherwise users would just have to wrap the call in Promise.resolve() if they want to use these features.

The callbacks passed to then() would be called like the callback. The error callback is called with the error, the success callback with an object {results, fields} just like the arguments for the callback are named in the docs.

The performance of this is negligible in the context of the IO roundtrips, as already mentioned here: #929. If a callback is passed, the functions should not return a promise/expose .then() so if you absolutely love callbacks you can use them without a performance impact.

A simple example: Adding a JSON object of a book with an array of authors into a db with transactions and proper error handling.

This is how we would write it today. Total callback hell.

let book = {name: 'test', authors: ['author1', 'author2']};
let db = pool.getConnection((err, db) => {
  if (err) {
    return req.statusCode = 500;
  }
  db.beginTransaction(err => {
    if (err) {
      req.statusCode = 500;
      return db.release();
    }
    db.query('INSERT INTO books (name) VALUES (?)', [book.name], (err, result) => {
      if (err) {
        req.statusCode = 500;
        return db.rollback(err => {
          db.release();          
        });
      }
      let bookId = results.id;
      let completed = 0;
      let errorHappened = false;
      for (let author of book.authors) {
        db.query('INSERT INTO book_authors (book_id, name) VALUES (?, ?)', [bookId, author], (err => result) {
          if (err) {
            req.statusCode = 500;
            errorHappened = true;
            return db.rollback(err => {
              db.release();              
            });
          }
          completed++;
          if (completed === book.authors.length) {
            db.commit(err => {
              if (err) {
                req.statusCode = 500;
              }
              db.release();              
            });
          }
        });
      }
    });    
  });
});

It gets slightly better with promises, because you get error handling and parallel execution for free:

let book = {name: 'test', authors: ['author1', 'author2']};
pool.getConnection()
  .then(db => db.beginTransaction())
  .then(db => 
    db.query('INSERT INTO books (name) VALUES (?)', [book.name])
      .then(answer => {
        let bookId = answer.results.insertId;
        return Promise.all(book.authors.map(author => db.query('INSERT INTO book_authors (book_id, name) VALUES (?, ?)', [bookId, author])))
      })
      .then(() => db.commit())
      .catch(error => db.rollback().then(() => Promise.reject(error)))    
      .finally(() => db.release())
  )
  .catch(error => {
    req.statusCode = 500;
  })

But it gets even better with ES7 (you can use this today either with Babel or with co by replacing all awaits with yield):

let book = {name: 'test', authors: ['author1', 'author2']};
try {
  let db = await pool.getConnection();
  await db.beginTransaction();
  try {
    let bookId = (await db.query('INSERT INTO books (name) VALUES (?)', [book.name])).results.insertId;
    await* book.authors.map(author => db.query('INSERT INTO book_authors (book_id, name) VALUES (?, ?)', [bookId, author]));
    await db.commit();
    req.statusCode = 201;
  } catch (error) {
    await db.rollback();
    throw error;
  } finally {
    db.release()
  }
} catch (error) {
  req.statusCode = 500;
}

You can achieve this with bluebird today, but in every file, every test you write, you have to do the following and also add an Async suffix to all methods:

let mysql = require('mysql')
let Pool = require('mysql/lib/pool')
let Connection = require('mysql/lib/connection')
Promise.promisifyAll(Pool.prototype)
Promise.promisifyAll(Connection.prototype)

To sum it up:

  • add then() to the EventEmitter returned by query() and make all other async functions which don't return anything atm return a promise.
  • Don't return a promise / add .then() when called with a callback.
  • keep all the API the same.

I would make a PR implementing all of this. Thoughts?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions