[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2\njobs:\n  build:\n    machine: true\n    working_directory: ~/mysql-events\n    steps:\n      - checkout\n\n      - restore_cache:\n          keys:\n            - lib-dependencies-{{ checksum \"package.json\" }}\n            - lib-dependencies-\n\n      - run:\n          name: Setup Code Climate test-reporter\n          command: |\n            curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter\n            chmod +x ./cc-test-reporter\n\n      - run:\n          name: Install dependencies\n          command: npm install\n\n#      - run:\n#          name: Wait for DB\n#          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s\n\n      - save_cache:\n          paths:\n            - node_modules\n          key: lib-dependencies-{{ checksum \"package.json\" }}\n\n      - run:\n          name: Run tests\n          command: |\n            chmod +x scripts/test.sh\n            ./scripts/test.sh\n\n#      - run:\n#          name: Run tests/coverage\n#          command: |\n#            npm run coverage\n#            ./cc-test-reporter format-coverage -t lcov -o coverage.json coverage/lcov.info\n#            ./cc-test-reporter upload-coverage -i coverage.json\n"
  },
  {
    "path": ".codeclimate.yml",
    "content": "engines:\n  eslint:\n    enabled: true\n    channel: 'eslint-2'\n    checks:\n      import/no-unresolved:\n        enabled: false\n      new-cap:\n        enabled: false\nratings:\n  paths:\n  - '*.js'\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\n#trim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": ""
  },
  {
    "path": ".eslintrc.yml",
    "content": "extends: airbnb-base\nplugins:\n  - import\nrules:\n  no-shadow: off\n  import/no-dynamic-require: off\n  global-require: off\n  no-param-reassign: off\n  consistent-return: off\n  arrow-body-style: off\n  no-underscore-dangle: off\n  import/extensions: off\n  prefer-destructuring: off\n  strict: off\n  max-len: off\n  no-console: off\n  no-continue: off\n  no-return-assign: off\nglobals:\n  process: on\n  describe: on\n  beforeAll: on\n  afterAll: on\n  beforeEach: on\n  afterEach: on\n  suite: on\n  test: on\n  it: on\n"
  },
  {
    "path": ".gitattributes",
    "content": "# These settings are for any web project\n\n# Handle line endings automatically for files detected as text\n# and leave all files detected as binary untouched.\n# * text=auto\n# NOTE - originally I had the above line un-commented.  it caused me a lot of grief related to line endings because I was dealing with WordPress plugins and the website changing line endings out if a user modified a plugin through the web interface.  commenting this line out seems to have alleviated the git chaos where simply switching to a branch caused it to believe 500 files were modified.\n\n#\n# The above will handle all files NOT found below\n#\n\n#\n## These files are text and should be normalized (Convert crlf => lf)\n#\n\n# source code\n*.php text\n*.css text\n*.sass text\n*.scss text\n*.less text\n*.styl text\n*.js text\n*.coffee text\n*.json text\n*.htm text\n*.html text\n*.xml text\n*.svg text\n*.txt text\n*.ini text\n*.inc text\n*.pl text\n*.rb text\n*.py text\n*.scm text\n*.sql text\n*.sh text\n*.bat text\n\n# templates\n*.ejs text\n*.hbt text\n*.jade text\n*.haml text\n*.hbs text\n*.dot text\n*.tmpl text\n*.phtml text\n\n# server config\n.htaccess text\n\n# git config\n.gitattributes text\n.gitignore text\n.gitconfig text\n\n# code analysis config\n.jshintrc text\n.jscsrc text\n.jshintignore text\n.csslintrc text\n\n# misc config\n*.yaml text\n*.yml text\n.editorconfig text\n\n# build config\n*.npmignore text\n*.bowerrc text\n\n# Heroku\nProcfile text\n.slugignore text\n\n# Documentation\n*.md text\nLICENSE text\nAUTHORS text\n\n\n#\n## These files are binary and should be left untouched\n#\n\n# (binary is a macro for -text -diff)\n*.png binary\n*.jpg binary\n*.jpeg binary\n*.gif binary\n*.ico binary\n*.mov binary\n*.mp4 binary\n*.mp3 binary\n*.flv binary\n*.fla binary\n*.swf binary\n*.gz binary\n*.zip binary\n*.7z binary\n*.ttf binary\n*.eot binary\n*.woff binary\n*.pyc binary\n*.pdf binary\n"
  },
  {
    "path": ".gitignore",
    "content": "/.idea/\n/node_modules/\n*.iml\n/.env\ncoverage\n"
  },
  {
    "path": ".npmignore",
    "content": "/.vscode/\n/.idea/\n/.circleci/\n/example/\n/examples/\n.codeclimate.yml\n.editorconfig\n.eslintignore\n.eslintrc.yml\n.gitattributes\n.gitignore\nexample.js\n*.iml\n"
  },
  {
    "path": "AUTHORS",
    "content": "Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com> (https://github.com/rodrigogs)\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2018, Rodrigo Gomes da Silva\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# mysql-events\n[![CircleCI](https://circleci.com/gh/rodrigogs/mysql-events.svg)](https://circleci.com/gh/rodrigogs/mysql-events)\n[![Code Climate](https://codeclimate.com/github/rodrigogs/mysql-events/badges/gpa.svg)](https://codeclimate.com/github/rodrigogs/mysql-events)\n[![Test Coverage](https://codeclimate.com/github/rodrigogs/mysql-events/badges/coverage.svg)](https://codeclimate.com/github/rodrigogs/mysql-events/coverage)\n\nA [node.js](https://nodejs.org) package that watches a MySQL database and runs callbacks on matched events.\n\nThis package is based on the [original ZongJi](https://github.com/nevill/zongji) and the [original mysql-events](https://github.com/spencerlambert/mysql-events) modules. Please make sure that you meet the requirements described at [ZongJi](https://github.com/rodrigogs/zongji#installation), like MySQL binlog etc.\n\nCheck [@kuroski](https://github.com/kuroski)'s [mysql-events-ui](https://github.com/kuroski/mysql-events-ui) for a `mysql-events` UI implementation.\n\n## Install\n```sh\nnpm install @rodrigogs/mysql-events\n```\n\n## Quick Start\n```javascript\nconst mysql = require('mysql');\nconst MySQLEvents = require('@rodrigogs/mysql-events');\n\nconst program = async () => {\n  const connection = mysql.createConnection({\n    host: 'localhost',\n    user: 'root',\n    password: 'root',\n  });\n\n  const instance = new MySQLEvents(connection, {\n    startAtEnd: true,\n    excludedSchemas: {\n      mysql: true,\n    },\n  });\n\n  await instance.start();\n\n  instance.addTrigger({\n    name: 'TEST',\n    expression: '*',\n    statement: MySQLEvents.STATEMENTS.ALL,\n    onEvent: (event) => { // You will receive the events here\n      console.log(event);\n    },\n  });\n  \n  instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);\n  instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);\n};\n\nprogram()\n  .then(() => console.log('Waiting for database events...'))\n  .catch(console.error);\n```\n[Check the examples](https://github.com/rodrigogs/mysql-events/examples)\n\n## Usage\n  ### #constructor(connection, options)\n  - Instantiate and create a database connection using a DSN\n    ```javascript\n    const dsn = {\n      host: 'localhost',\n      user: 'username',\n      password: 'password',\n    };\n\n    const myInstance = new MySQLEvents(dsn, { /* ZongJi options */ });\n    ```\n\n  - Instantiate and create a database connection using a preexisting connection\n    ```javascript\n    const connection = mysql.createConnection({\n      host: 'localhost',\n      user: 'username',\n      password: 'password',\n    });\n\n    const myInstance = new MySQLEvents(connection, { /* ZongJi options */ });\n    ```\n  - Options(the second argument) is for ZongJi options\n    ```javascript\n    const myInstance = new MySQLEvents({ /* connection */ }, {\n      serverId: 3,\n      startAtEnd: true,\n    });\n    ```\n    [See more about ZongJi options](https://github.com/rodrigogs/zongji#zongji-class)\n\n  ### #start()\n  - start function ensures that MySQL is connected and ZongJi is running before resolving its promise\n    ```javascript\n    myInstance.start()\n      .then(() => console.log('I\\'m running!'))\n      .catch(err => console.error('Something bad happened', err));\n    ```\n  ### #stop()\n  - stop function terminates MySQL connection and stops ZongJi before resolving its promise\n    ```javascript\n    myInstance.stop()\n      .then(() => console.log('I\\'m stopped!'))\n      .catch(err => console.error('Something bad happened', err));\n    ```\n  ### #pause()\n  - pause function pauses MySQL connection until `#resume()` is called, this it useful when you're receiving more data than you can handle at the time\n    ```javascript\n    myInstance.pause();\n    ```\n  ### #resume()\n  - resume function resumes a paused MySQL connection, so it starts to generate binlog events again\n    ```javascript\n    myInstance.resume();\n    ```\n  ### #addTrigger({ name, expression, statement, onEvent })\n  - Adds a trigger for the given expression/statement and calls the `onEvent` function when the event happens\n    ```javascript\n    instance.addTrigger({\n      name: 'MY_TRIGGER',\n      expression: 'MY_SCHEMA.MY_TABLE.MY_COLUMN',\n      statement: MySQLEvents.STATEMENTS.INSERT,\n      onEvent: async (event) => {\n        // Here you will get the events for the given expression/statement.\n        // This could be an async function.\n        await doSomething(event);\n      },\n    });\n    ```\n  - The `name` argument must be unique for each expression/statement, it will be user later if you want to remove a trigger\n    ```javascript\n    instance.addTrigger({\n      name: 'MY_TRIGGER',\n      expression: 'MY_SCHEMA.*',\n      statement: MySQLEvents.STATEMENTS.ALL,\n      ...\n    });\n\n    instance.removeTrigger({\n      name: 'MY_TRIGGER',\n      expression: 'MY_SCHEMA.*',\n      statement: MySQLEvents.STATEMENTS.ALL,\n    });\n    ```\n  - The `expression` argument is very dynamic, you can replace any step by `*` to make it wait for any schema, table or column events\n    ```javascript\n    instance.addTrigger({\n      name: 'Name updates from table USERS at SCHEMA2',\n      expression: 'SCHEMA2.USERS.name',\n      ...\n    });\n    ```\n    ```javascript\n    instance.addTrigger({\n      name: 'All database events',\n      expression: '*',\n      ...\n    });\n    ```\n    ```javascript\n    instance.addTrigger({\n      name: 'All events from SCHEMA2',\n      expression: 'SCHEMA2.*',\n      ...\n    });\n    ```\n    ```javascript\n    instance.addTrigger({\n      name: 'All database events for table USERS',\n      expression: '*.USERS',\n      ...\n    });\n    ```\n  - The `statement` argument indicates in which database operation an event should be triggered\n    ```javascript\n    instance.addTrigger({\n      ...\n      statement: MySQLEvents.STATEMENTS.ALL,\n      ...\n    });\n    ```\n    [Allowed statements](https://github.com/rodrigogs/mysql-events/blob/master/lib/STATEMENTS.enum.js)\n  - The `onEvent` argument is a function where the trigger events should be threated\n    ```javascript\n    instance.addTrigger({\n      ...\n      onEvent: (event) => {\n        console.log(event); // { type, schema, table, affectedRows: [], affectedColumns: [], timestamp, }\n      },\n      ...\n    });\n    ```\n  ### #removeTrigger({ name, expression, statement })\n  - Removes a trigger from the current instance\n    ```javascript\n    instance.removeTrigger({\n      name: 'My previous created trigger',\n      expression: '',\n      statement: MySQLEvents.STATEMENTS.INSERT,\n    });\n    ```\n  ### Instance events\n  - MySQLEvents class emits some events related to its MySQL connection and ZongJi instance\n    ```javascript\n    instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, (err) => console.log('Connection error', err));\n    instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, (err) => console.log('ZongJi error', err));\n    ```\n  [Available events](https://github.com/rodrigogs/mysql-events/blob/master/lib/EVENTS.enum.js)\n\n## Tigger event object\nIt has the following structure:\n```javascript\n{\n  type: 'INSERT | UPDATE | DELETE',\n  schema: 'SCHEMA_NAME',\n  table: 'TABLE_NAME',\n  affectedRows: [{\n    before: {\n      column1: 'A',\n      column2: 'B',\n      column3: 'C',\n      ...\n    },\n    after: {\n      column1: 'D',\n      column2: 'E',\n      column3: 'F',\n      ...\n    },\n  }],\n  affectedColumns: [\n    'column1',\n    'column2',\n    'column3',\n  ],\n  timestamp: 1530645380029,\n  nextPosition: 1343,\n  binlogName: 'bin.001',\n}\n```\n\n**Make sure the database user has the privilege to read the binlog on database that you want to watch on.**\n\n## LICENSE\n[BSD-3-Clause](https://github.com/rodrigogs/mysql-events/blob/master/LICENSE) © Rodrigo Gomes da Silva\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "---\nversion: '2'\nservices:\n  mysql55:\n    image: mysql:5.5\n    container_name: mysql55\n    command: [ \"--server-id=1\", \"--log-bin=/var/lib/mysql/mysql-bin.log\", \"--binlog-format=row\"]\n    ports:\n      - 3355:3306\n    networks:\n      default:\n        aliases:\n          - mysql55\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n\n  mysql56:\n    image: mysql:5.6\n    container_name: mysql56\n    command: [ \"--server-id=1\", \"--log-bin=/var/lib/mysql/mysql-bin.log\", \"--binlog-format=row\"]\n    ports:\n      - 3356:3306\n    networks:\n      default:\n        aliases:\n          - mysql56\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n\n  mysql57:\n    image: mysql:5.7\n    container_name: mysql57\n    command: [ \"--server-id=1\", \"--log-bin=/var/lib/mysql/mysql-bin.log\", \"--binlog-format=row\"]\n    ports:\n      - 3357:3306\n    networks:\n      default:\n        aliases:\n          - mysql57\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n\n  mysql80:\n    image: mysql:8.0\n    container_name: mysql80\n    command: [ \"--server-id=1\", \"--log-bin=/var/lib/mysql/mysql-bin.log\", \"--binlog-format=row\", \"--default-authentication-plugin=mysql_native_password\"]\n    ports:\n      - 3380:3306\n    networks:\n      default:\n        aliases:\n          - mysql80\n    environment:\n      MYSQL_ROOT_PASSWORD: root\n"
  },
  {
    "path": "examples/watchWholeInstance.js",
    "content": "const MySQLEvents = require('@rodrigogs/mysql-events');\n\nconst program = async () => {\n  const instance = new MySQLEvents({\n    host: 'localhost',\n    user: 'root',\n    password: 'root',\n  }, {\n    startAtEnd: true,\n  });\n\n  await instance.start();\n\n  instance.addTrigger({\n    name: 'Whole database instance',\n    expression: '*',\n    statement: MySQLEvents.STATEMENTS.ALL,\n    onEvent: (event) => {\n      console.log(event);\n    },\n  });\n\n  instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);\n  instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);\n};\n\nprogram()\n  .then(() => console.log('Waiting for database vents...'))\n  .catch(console.error);\n"
  },
  {
    "path": "index.js",
    "content": "module.exports = require('./lib');\n"
  },
  {
    "path": "lib/EVENTS.enum.js",
    "content": "const EVENTS = {\n  STARTED: 'started',\n  STOPPED: 'stopped',\n  PAUSED: 'paused',\n  RESUMED: 'resumed',\n  BINLOG: 'binlog',\n  TRIGGER_ERROR: 'triggerError',\n  CONNECTION_ERROR: 'connectionError',\n  ZONGJI_ERROR: 'zongjiError',\n};\n\nmodule.exports = EVENTS;\n"
  },
  {
    "path": "lib/MySQLEvents.js",
    "content": "const debug = require('debuggler')();\nconst ZongJi = require('@rodrigogs/zongji');\nconst EventEmitter = require('events');\nconst eventHandler = require('./eventHandler');\nconst connectionHandler = require('./connectionHandler');\n\nconst EVENTS = require('./EVENTS.enum');\nconst STATEMENTS = require('./STATEMENTS.enum');\n\n/**\n * @param {Object|Connection|String} connection\n * @param {Object} options\n */\nclass MySQLEvents extends EventEmitter {\n  constructor(connection, options = {}) {\n    super();\n\n    this.connection = connection;\n    this.options = options;\n\n    this.isStarted = false;\n    this.isPaused = false;\n\n    this.zongJi = null;\n    this.expressions = {};\n  }\n\n  /**\n   * @return {{BINLOG, TRIGGER_ERROR, CONNECTION_ERROR, ZONGJI_ERROR}}\n   * @constructor\n   */\n  static get EVENTS() {\n    return EVENTS;\n  }\n\n  /**\n   * @return {{ALL: string, INSERT: string, UPDATE: string, DELETE: string}}\n   */\n  static get STATEMENTS() {\n    return STATEMENTS;\n  }\n\n  /**\n   * @param {Object} event binlog event object.\n   * @private\n   */\n  _handleEvent(event) {\n    if (!this.zongJi) return;\n\n    event.binlogName = this.zongJi.binlogName;\n    event = eventHandler.normalizeEvent(event);\n    const triggers = eventHandler.findTriggers(event, this.expressions);\n\n    Promise.all(triggers.map(async (trigger) => {\n      try {\n        await trigger.onEvent(event);\n      } catch (error) {\n        this.emit(EVENTS.TRIGGER_ERROR, { trigger, error });\n      }\n    })).then(() => debug('triggers executed'));\n  }\n\n  /**\n   * @private\n   */\n  _handleZongJiEvents() {\n    this.zongJi.on('error', err => this.emit(EVENTS.ZONGJI_ERROR, err));\n    this.zongJi.on('binlog', (event) => {\n      this.emit(EVENTS.BINLOG, event);\n      this._handleEvent(event);\n    });\n  }\n\n  /**\n   * @private\n   */\n  _handleConnectionEvents() {\n    this.connection.on('error', err => this.emit(EVENTS.CONNECTION_ERROR, err));\n  }\n\n  /**\n   * @param {Object} [options = {}]\n   * @return {Promise<void>}\n   */\n  async start(options = {}) {\n    if (this.isStarted) return;\n    debug('connecting to mysql');\n    this.connection = await connectionHandler(this.connection);\n\n    debug('initializing zongji');\n    this.zongJi = new ZongJi(this.connection, Object.assign({}, this.options, options));\n\n    debug('connected');\n    this.emit('connected');\n    this._handleConnectionEvents();\n    this._handleZongJiEvents();\n    this.zongJi.start(this.options);\n    this.isStarted = true;\n    this.emit(EVENTS.STARTED);\n  }\n\n  /**\n   * @return {Promise<void>}\n   */\n  async stop() {\n    if (!this.isStarted) return;\n    debug('disconnecting from mysql');\n\n    this.zongJi.stop();\n    delete this.zongJi;\n\n    await new Promise((resolve, reject) => {\n      this.connection.end((err) => {\n        if (err) return reject(err);\n        resolve();\n      });\n    });\n\n    debug('disconnected');\n    this.emit('disconnected');\n    this.isStarted = false;\n    this.emit(EVENTS.STOPPED);\n  }\n\n  /**\n   *\n   */\n  pause() {\n    if (!this.isStarted || this.isPaused) return;\n    debug('pausing connection');\n\n    this.zongJi.connection.pause();\n    this.isPaused = true;\n    this.emit(EVENTS.PAUSED);\n  }\n\n  /**\n   *\n   */\n  resume() {\n    if (!this.isStarted || !this.isPaused) return;\n    debug('resuming connection');\n\n    this.zongJi.connection.resume();\n    this.isPaused = false;\n    this.emit(EVENTS.RESUMED);\n  }\n\n  /**\n   * @param {String} name\n   * @param {String} expression\n   * @param {String} [statement = 'ALL']\n   * @param {Function} [onEvent]\n   * @return {void}\n   */\n  addTrigger({\n    name,\n    expression,\n    statement = STATEMENTS.ALL,\n    onEvent,\n  }) {\n    if (!name) throw new Error('Missing trigger name');\n    if (!expression) throw new Error('Missing trigger expression');\n    if (typeof onEvent !== 'function') throw new Error('onEvent argument should be a function');\n\n    this.expressions[expression] = this.expressions[expression] || {};\n    this.expressions[expression].statements = this.expressions[expression].statements || {};\n    this.expressions[expression].statements[statement] = this.expressions[expression].statements[statement] || [];\n\n    const triggers = this.expressions[expression].statements[statement];\n    if (triggers.find(st => st.name === name)) {\n      throw new Error(`There's already a trigger named \"${name}\" for expression \"${expression}\" with statement \"${statement}\"`);\n    }\n\n    triggers.push({\n      name,\n      onEvent,\n    });\n  }\n\n  /**\n   * @param {String} name\n   * @param {String} expression\n   * @param {String} [statement = 'ALL']\n   * @return {void}\n   */\n  removeTrigger({\n    name,\n    expression,\n    statement = STATEMENTS.ALL,\n  }) {\n    const exp = this.expressions[expression];\n    if (!exp) return;\n\n    const triggers = exp.statements[statement];\n    if (!triggers) return;\n\n    const named = triggers.find(st => st.name === name);\n    if (!named) return;\n\n    const index = triggers.indexOf(named);\n    triggers.splice(index, 1);\n  }\n}\n\nmodule.exports = MySQLEvents;\n"
  },
  {
    "path": "lib/STATEMENTS.enum.js",
    "content": "const STATEMENTS = {\n  ALL: 'ALL',\n  INSERT: 'INSERT',\n  UPDATE: 'UPDATE',\n  DELETE: 'DELETE',\n};\n\nmodule.exports = STATEMENTS;\n"
  },
  {
    "path": "lib/connectionHandler.js",
    "content": "const debug = require('debuggler')();\nconst mysql = require('mysql');\nconst Connection = require('mysql/lib/Connection');\nconst Pool = require('mysql/lib/Pool');\n\nconst connect = connection => new Promise((resolve, reject) => connection.connect((err) => {\n  if (err) return reject(err);\n  resolve();\n}));\n\nconst connectionHandler = async (connection) => {\n  if (connection instanceof Pool) {\n    debug('reusing pool:', connection);\n    if (connection._closed) {\n      connection = mysql.createPool(connection.config.connectionConfig);\n    }\n  }\n\n  if (connection instanceof Connection) {\n    debug('reusing connection:', connection);\n    if (connection.state !== 'connected') {\n      connection = mysql.createConnection(connection.config);\n    }\n  }\n\n  if (typeof connection === 'string') {\n    debug('creating connection from string:', connection);\n    connection = mysql.createConnection(connection);\n  }\n\n  if ((typeof connection === 'object') && (!(connection instanceof Connection) && !(connection instanceof Pool))) {\n    debug('creating connection from object:', connection);\n    if (connection.isPool) {\n      connection = mysql.createPool(connection);\n    } else {\n      connection = mysql.createConnection(connection);\n    }\n  }\n\n  if ((connection instanceof Connection) && (connection.state !== 'connected')) {\n    debug('initializing connection');\n    await connect(connection);\n  }\n\n  return connection;\n};\n\nmodule.exports = connectionHandler;\n"
  },
  {
    "path": "lib/dataNormalizer.js",
    "content": "const STATEMENTS = require('./STATEMENTS.enum');\n\nconst getEventType = (eventName) => {\n  return {\n    writerows: STATEMENTS.INSERT,\n    updaterows: STATEMENTS.UPDATE,\n    deleterows: STATEMENTS.DELETE,\n  }[eventName];\n};\n\nconst normalizeRow = (row) => {\n  if (!row) return undefined;\n\n  const columns = Object.getOwnPropertyNames(row);\n  for (let i = 0, len = columns.length; i < len; i += 1) {\n    const columnValue = row[columns[i]];\n\n    if (columnValue instanceof Buffer && columnValue.length === 1) { // It's a boolean\n      row[columns[i]] = (columnValue[0] > 0);\n    }\n  }\n\n  return row;\n};\n\nconst hasDifference = (beforeValue, afterValue) => {\n  if ((beforeValue && afterValue) && beforeValue instanceof Date) {\n    return beforeValue.getTime() !== afterValue.getTime();\n  }\n\n  return beforeValue !== afterValue;\n};\n\nconst fixRowStructure = (type, row) => {\n  if (type === STATEMENTS.INSERT) {\n    row = {\n      before: undefined,\n      after: row,\n    };\n  }\n  if (type === STATEMENTS.DELETE) {\n    row = {\n      before: row,\n      after: undefined,\n    };\n  }\n\n  return row;\n};\n\nconst resolveAffectedColumns = (normalizedEvent, normalizedRows) => {\n  const columns = Object.getOwnPropertyNames((normalizedRows.after || normalizedRows.before));\n  for (let i = 0, len = columns.length; i < len; i += 1) {\n    const columnName = columns[i];\n    const beforeValue = (normalizedRows.before || {})[columnName];\n    const afterValue = (normalizedRows.after || {})[columnName];\n\n    if (hasDifference(beforeValue, afterValue)) {\n      if (normalizedEvent.affectedColumns.indexOf(columnName) === -1) {\n        normalizedEvent.affectedColumns.push(columnName);\n      }\n    }\n  }\n};\n\nconst dataNormalizer = (event) => {\n  const type = getEventType(event.getEventName());\n  const schema = event.tableMap[event.tableId].parentSchema;\n  const table = event.tableMap[event.tableId].tableName;\n  const { timestamp, nextPosition, binlogName } = event;\n\n  const normalized = {\n    type,\n    schema,\n    table,\n    affectedRows: [],\n    affectedColumns: [],\n    timestamp,\n    nextPosition,\n    binlogName,\n  };\n\n  event.rows.forEach((row) => {\n    row = fixRowStructure(type, row);\n\n    const normalizedRows = {\n      after: normalizeRow(row.after),\n      before: normalizeRow(row.before),\n    };\n\n    normalized.affectedRows.push(normalizedRows);\n\n    resolveAffectedColumns(normalized, normalizedRows);\n  });\n\n  return normalized;\n};\n\nmodule.exports = dataNormalizer;\n"
  },
  {
    "path": "lib/eventHandler.js",
    "content": "const normalize = require('./dataNormalizer');\nconst STATEMENTS = require('./STATEMENTS.enum');\n\nconst parseExpression = (expression = '') => {\n  const parts = expression.split('.');\n  return {\n    schema: parts[0],\n    table: parts[1],\n    column: parts[2],\n    value: parts[3],\n  };\n};\n\nconst normalizeEvent = (event) => {\n  const dataEvents = [\n    'writerows',\n    'updaterows',\n    'deleterows',\n  ];\n\n  if (dataEvents.indexOf(event.getEventName()) !== -1) {\n    return normalize(event);\n  }\n\n  return event;\n};\n\n/**\n * @param {Object} event\n * @param {Object} triggers\n * @return {Object[]}\n */\nconst findTriggers = (event, triggers) => {\n  if (!event.type) return [];\n\n  const triggerExpressions = Object.getOwnPropertyNames(triggers);\n  const statements = [];\n\n  for (let i = 0, len = triggerExpressions.length; i < len; i += 1) {\n    const expression = triggerExpressions[i];\n    const trigger = triggers[expression];\n\n    const parts = parseExpression(expression);\n    if (parts.schema !== '*' && parts.schema !== event.schema) continue;\n    if (!(!parts.table || parts.table === '*') && parts.table !== event.table) continue;\n    if (!(!parts.column || parts.column === '*') && event.affectedColumns.indexOf(parts.column) === -1) continue;\n\n    if (trigger.statements[STATEMENTS.ALL]) statements.push(...trigger.statements[STATEMENTS.ALL]);\n    if (trigger.statements[event.type]) statements.push(...trigger.statements[event.type]);\n  }\n\n  return statements;\n};\n\n/**\n * @type {{normalizeEvent: normalizeEvent, findTriggers: findTriggers}}\n */\nconst eventHandler = {\n  normalizeEvent,\n  findTriggers,\n};\n\nmodule.exports = eventHandler;\n"
  },
  {
    "path": "lib/index.js",
    "content": "module.exports = require('./MySQLEvents');\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@rodrigogs/mysql-events\",\n  \"version\": \"0.6.0\",\n  \"license\": \"BSD-3-Clause\",\n  \"description\": \"A node.js package that watches a MySQL database and runs callbacks on matched events like updates on tables and/or specific columns.\",\n  \"homepage\": \"https://github.com/rodrigogs/mysql-events\",\n  \"keywords\": [\n    \"mysql\",\n    \"events\",\n    \"trigger\",\n    \"notify\",\n    \"watcher\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:rodrigogs/mysql-events.git\"\n  },\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"eslint\": \"eslint . --ext .js\",\n    \"test\": \"npm run test:55 && npm run test:56 && npm run test:57 && npm run test:80\",\n    \"test:55\": \"cross-env DATABASE_PORT=3355 jest --forceExit --runInBand\",\n    \"test:56\": \"cross-env DATABASE_PORT=3356 jest --forceExit --runInBand\",\n    \"test:57\": \"cross-env DATABASE_PORT=3357 jest --forceExit --runInBand\",\n    \"test:80\": \"cross-env DATABASE_PORT=3380 jest --forceExit --runInBand\",\n    \"test:local\": \"./scripts/test.sh\",\n    \"coverage\": \"nyc --reporter=lcov npm test\"\n  },\n  \"dependencies\": {\n    \"@rodrigogs/zongji\": \"^0.4.14\",\n    \"debug\": \"^4.1.1\",\n    \"debuggler\": \"^1.0.0\",\n    \"mysql\": \"^2.17.1\"\n  },\n  \"devDependencies\": {\n    \"chai\": \"^4.2.0\",\n    \"codeclimate-test-reporter\": \"^0.5.1\",\n    \"cross-env\": \"^5.2.0\",\n    \"dotenv-cli\": \"^2.0.0\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-config-airbnb-base\": \"^13.1.0\",\n    \"eslint-plugin-import\": \"^2.17.2\",\n    \"jest\": \"^24.8.0\",\n    \"nyc\": \"^14.1.1\"\n  },\n  \"engines\": {\n    \"node\": \">=7.6.0\"\n  }\n}\n"
  },
  {
    "path": "scripts/test.sh",
    "content": "#!/bin/bash\n\n### CONFIG\nexport MSYS_NO_PATHCONV=1; # git-bash workaroung for Windows\n\nmy_dir=\"$(dirname \"$0\")\";\n\n### PROGRAM\ndocker-compose up -d;\necho 'Waiting for docker services...';\nwhile ! docker exec mysql55 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done\nwhile ! docker exec mysql56 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done\nwhile ! docker exec mysql57 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done\nwhile ! docker exec mysql80 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done\n\n### SINGLE CONNECTION\ndocker run --rm -t \\\n  --net=host \\\n  -v `pwd`:/app \\\n  -w /app node:8-alpine \\\n  /bin/sh -c \"npm install && npm test\"\n\nexitCode=$?;\n\n### CONNECTION POOL\nif [ \"$exitCode\" == \"0\" ]; then\n  docker run --rm -t \\\n    --net=host \\\n    -v `pwd`:/app \\\n    -w /app node:8-alpine \\\n    /bin/sh -c \"export IS_POOL=true; npm test\"\n\n  exitCode=$?;\nfi\n\ndocker-compose down -v;\n\nexit $exitCode;\n"
  },
  {
    "path": "test.js",
    "content": "/* eslint-disable padded-blocks,no-unused-expressions,no-await-in-loop */\n\nconst chai = require('chai');\nconst mysql = require('mysql');\nconst MySQLEvents = require('./lib');\n\nconst { expect } = chai;\n\nconst DATABASE_PORT = process.env.DATABASE_PORT || 3306;\nconst IS_POOL = process.env.IS_POOL || false;\nconst TEST_SCHEMA_1 = 'testSchema1';\nconst TEST_SCHEMA_2 = 'testSchema2';\nconst TEST_TABLE_1 = 'testTable1';\nconst TEST_TABLE_2 = 'testTable2';\nconst TEST_COLUMN_1 = 'column1';\nconst TEST_COLUMN_2 = 'column2';\n\nconst delay = (timeout = 500) => new Promise((resolve) => {\n  setTimeout(resolve, timeout);\n});\n\nlet _serverId = 0;\nconst getServerId = () => {\n  return _serverId += 1;\n};\n\nconst getConnection = () => {\n  const connection = mysql.createConnection({\n    host: 'localhost',\n    user: 'root',\n    password: 'root',\n    port: DATABASE_PORT,\n  });\n\n  return new Promise((resolve, reject) => connection.connect((err) => {\n    if (err) return reject(err);\n    resolve(connection);\n  }));\n};\n\nconst executeQuery = (conn, query) => {\n  return new Promise((resolve, reject) => conn.query(query, (err, results) => {\n    if (err) return reject(err);\n    resolve(results);\n  }));\n};\n\nconst closeConnection = conn => new Promise((resolve, reject) => conn.end((err) => {\n  if (err) return reject(err);\n  resolve();\n}));\n\nconst grantPrivileges = async () => {\n  const conn = await getConnection();\n  try {\n    await executeQuery(conn, 'GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO \\'root\\'@\\'localhost\\'');\n  } catch (err) {\n    throw err;\n  } finally {\n    await closeConnection(conn);\n  }\n};\n\nconst createSchemas = async () => {\n  console.log('Creating connection...');\n  const conn = await getConnection();\n  try {\n    await executeQuery(conn, `CREATE DATABASE IF NOT EXISTS ${TEST_SCHEMA_1};`);\n    await executeQuery(conn, `CREATE DATABASE IF NOT EXISTS ${TEST_SCHEMA_2};`);\n  } catch (err) {\n    throw err;\n  } finally {\n    await closeConnection(conn);\n  }\n};\n\nconst dropSchemas = async () => {\n  const conn = await getConnection();\n  try {\n    await executeQuery(conn, `DROP DATABASE IF EXISTS ${TEST_SCHEMA_1};`);\n    await executeQuery(conn, `DROP DATABASE IF EXISTS ${TEST_SCHEMA_2};`);\n  } catch (err) {\n    throw err;\n  } finally {\n    await closeConnection(conn);\n  }\n};\n\nconst createTables = async () => {\n  const conn = await getConnection();\n  try {\n    await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_1} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);\n    await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_2} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);\n    await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_1} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);\n    await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_2} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);\n  } catch (err) {\n    throw err;\n  } finally {\n    await closeConnection(conn);\n  }\n};\n\nconst dropTables = async () => {\n  const conn = await getConnection();\n  try {\n    await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_1};`);\n    await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_2};`);\n    await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_1};`);\n    await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_2};`);\n  } catch (err) {\n    throw err;\n  } finally {\n    await closeConnection(conn);\n  }\n};\n\nbeforeAll(async () => {\n  console.log(`Runnning tests on port ${DATABASE_PORT}...`);\n\n  chai.should();\n  await createSchemas();\n  await grantPrivileges();\n});\n\nbeforeEach(async () => {\n  await createTables();\n});\n\nafterEach(async () => {\n  await dropTables();\n});\n\nafterAll(async () => {\n  await dropSchemas();\n});\n\ndescribe(`MySQLEvents using ${IS_POOL ? 'connection pool' : 'single connection'} on port ${DATABASE_PORT}`, () => {\n\n  it('should expose EVENTS enum', async () => {\n    MySQLEvents.EVENTS.should.be.an('object');\n    MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('BINLOG');\n    MySQLEvents.EVENTS.BINLOG.should.be.equal('binlog');\n    MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('TRIGGER_ERROR');\n    MySQLEvents.EVENTS.TRIGGER_ERROR.should.be.equal('triggerError');\n    MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('CONNECTION_ERROR');\n    MySQLEvents.EVENTS.CONNECTION_ERROR.should.be.equal('connectionError');\n    MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('ZONGJI_ERROR');\n    MySQLEvents.EVENTS.ZONGJI_ERROR.should.be.equal('zongjiError');\n  });\n\n  it('should expose STATEMENTS enum', async () => {\n    MySQLEvents.STATEMENTS.should.be.an('object');\n    MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('ALL');\n    MySQLEvents.STATEMENTS.ALL.should.be.equal('ALL');\n    MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('INSERT');\n    MySQLEvents.STATEMENTS.INSERT.should.be.equal('INSERT');\n    MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('UPDATE');\n    MySQLEvents.STATEMENTS.UPDATE.should.be.equal('UPDATE');\n    MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('DELETE');\n    MySQLEvents.STATEMENTS.DELETE.should.be.equal('DELETE');\n  });\n\n  it('should connect and disconnect from MySQL using a pre existing connection', async () => {\n    let connection;\n    if (IS_POOL) {\n      connection = mysql.createPool({\n        host: 'localhost',\n        user: 'root',\n        password: 'root',\n        port: DATABASE_PORT,\n      });\n    } else {\n      connection = mysql.createConnection({\n        host: 'localhost',\n        user: 'root',\n        password: 'root',\n        port: DATABASE_PORT,\n      });\n    }\n\n    const instance = new MySQLEvents(connection);\n\n    await instance.start();\n\n    await delay();\n\n    await instance.stop();\n  }, 10000);\n\n  it('should connect and disconnect from MySQL using a dsn', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    });\n\n    await instance.start();\n\n    await delay();\n\n    await instance.stop();\n  }, 10000);\n\n  it('should connect and disconnect from MySQL using a connection string', async () => {\n    const instance = new MySQLEvents(`mysql://root:root@localhost:${DATABASE_PORT}/${TEST_SCHEMA_1}`);\n\n    await instance.start();\n\n    await delay();\n\n    await instance.stop();\n  }, 10000);\n\n  it('should catch an event using an INSERT trigger', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggerEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.INSERT,\n      onEvent: event => triggerEvents.push(event),\n    });\n\n    instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, console.error);\n    instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);\n    instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n\n    await delay(5000);\n\n    if (!triggerEvents.length) throw new Error('No trigger was caught');\n\n    triggerEvents[0].should.be.an('object');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('type');\n    triggerEvents[0].type.should.be.a('string').equals('INSERT');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');\n    triggerEvents[0].timestamp.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('table');\n    triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('schema');\n    triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');\n    triggerEvents[0].nextPosition.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');\n    triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);\n    triggerEvents[0].affectedRows[0].should.be.an('object');\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');\n    triggerEvents[0].affectedRows[0].after.should.be.an('object');\n    triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_1);\n    triggerEvents[0].affectedRows[0].after[TEST_COLUMN_1].should.be.a('string').equals('test1');\n    triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_2);\n    triggerEvents[0].affectedRows[0].after[TEST_COLUMN_2].should.be.a('string').equals('test2');\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');\n    expect(triggerEvents[0].affectedRows[0].before).to.be.an('undefined');\n\n    await instance.stop();\n  }, 15000);\n\n  it('should catch an event using an UPDATE trigger', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggerEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.UPDATE,\n      onEvent: event => triggerEvents.push(event),\n    });\n\n    instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, console.error);\n    instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);\n    instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);\n\n    await delay(5000);\n\n    if (!triggerEvents.length) throw new Error('No trigger was caught');\n\n    triggerEvents[0].should.be.an('object');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('type');\n    triggerEvents[0].type.should.be.a('string').equals('UPDATE');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');\n    triggerEvents[0].timestamp.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('table');\n    triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('schema');\n    triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');\n    triggerEvents[0].nextPosition.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');\n    triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);\n    triggerEvents[0].affectedRows[0].should.be.an('object');\n\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');\n    triggerEvents[0].affectedRows[0].after.should.be.an('object');\n    triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_1);\n    triggerEvents[0].affectedRows[0].after[TEST_COLUMN_1].should.be.a('string').equals('test3');\n    triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_2);\n    triggerEvents[0].affectedRows[0].after[TEST_COLUMN_2].should.be.a('string').equals('test4');\n\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');\n    triggerEvents[0].affectedRows[0].before.should.be.an('object');\n    triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_1);\n    triggerEvents[0].affectedRows[0].before[TEST_COLUMN_1].should.be.a('string').equals('test1');\n    triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_2);\n    triggerEvents[0].affectedRows[0].before[TEST_COLUMN_2].should.be.a('string').equals('test2');\n\n    await instance.stop();\n  }, 15000);\n\n  it('should catch an event using a DELETE trigger', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggerEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.DELETE,\n      onEvent: event => triggerEvents.push(event),\n    });\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(instance.connection, `DELETE FROM ${TEST_SCHEMA_1}.${TEST_TABLE_1} WHERE ${TEST_COLUMN_1} = 'test1' AND ${TEST_COLUMN_2} = 'test2';`);\n\n    await delay(5000);\n\n    if (!triggerEvents.length) throw new Error('No trigger was caught');\n\n    triggerEvents[0].should.be.an('object');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('type');\n    triggerEvents[0].type.should.be.a('string').equals('DELETE');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');\n    triggerEvents[0].timestamp.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('table');\n    triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('schema');\n    triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');\n    triggerEvents[0].nextPosition.should.be.a('number');\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');\n    triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);\n    triggerEvents[0].affectedRows[0].should.be.an('object');\n\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');\n    expect(triggerEvents[0].affectedRows[0].after).to.be.an('undefined');\n\n    triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');\n    triggerEvents[0].affectedRows[0].before.should.be.an('object');\n    triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_1);\n    triggerEvents[0].affectedRows[0].before[TEST_COLUMN_1].should.be.a('string').equals('test1');\n    triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_2);\n    triggerEvents[0].affectedRows[0].before[TEST_COLUMN_2].should.be.a('string').equals('test2');\n\n    await instance.stop();\n  }, 15000);\n\n  it('should catch events using an ALL trigger', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggerEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: event => triggerEvents.push(event),\n    });\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);\n    await executeQuery(instance.connection, `DELETE FROM ${TEST_SCHEMA_1}.${TEST_TABLE_1} WHERE ${TEST_COLUMN_1} = 'test3' AND ${TEST_COLUMN_2} = 'test4';`);\n\n    await delay(1000);\n\n    expect(triggerEvents).to.be.an('array').that.is.not.empty;\n\n    triggerEvents[0].should.have.ownPropertyDescriptor('type');\n    triggerEvents[0].type.should.be.a('string').equals('INSERT');\n\n    triggerEvents[1].should.have.ownPropertyDescriptor('type');\n    triggerEvents[1].type.should.be.a('string').equals('UPDATE');\n\n    triggerEvents[2].should.have.ownPropertyDescriptor('type');\n    triggerEvents[2].type.should.be.a('string').equals('DELETE');\n\n    await instance.stop();\n  }, 15000);\n\n  it('should remove a previously added event trigger', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    });\n\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: () => {},\n    });\n\n    instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL].should.be.an('array').that.is.not.empty;\n\n    instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].should.be.an('object');\n    instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].name.should.be.a('string').equals('Test');\n    instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].onEvent.should.be.a('function');\n\n    instance.removeTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n    });\n\n    expect(instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0]).to.be.an('undefined');\n\n    await instance.stop();\n  }, 10000);\n\n  it('should throw an error when adding duplicated trigger name for a statement', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    });\n\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: () => {},\n    });\n\n    expect(() => instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: () => {},\n    })).to.throw(Error);\n  });\n\n  it('should emit an event when a trigger produces an error', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    await delay();\n\n    let error = null;\n    instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, (err) => {\n      error = err;\n    });\n\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: () => {\n        throw new Error('Error');\n      },\n    });\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n\n    await delay(1000);\n\n    expect(error).to.be.an('object');\n    error.trigger.should.be.an('object');\n    error.error.should.be.an('Error');\n  }, 10000);\n\n  it('should receive events from multiple schemas', async () => {\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggeredEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}`,\n      statement: MySQLEvents.STATEMENTS.UPDATE,\n      onEvent: event => triggeredEvents.push(event),\n    });\n    instance.addTrigger({\n      name: 'Test2',\n      expression: `${TEST_SCHEMA_2}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: event => triggeredEvents.push(event),\n    });\n\n    await delay(5000);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);\n\n    await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_2}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_2}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);\n\n    await delay(1000);\n\n    if (!triggeredEvents.length) throw new Error('No trigger was caught');\n  }, 20000);\n\n  it('should pause and resume connection', async () => {\n    const connection = mysql.createConnection({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n    });\n\n    const instance = new MySQLEvents({\n      host: 'localhost',\n      user: 'root',\n      password: 'root',\n      port: DATABASE_PORT,\n      isPool: IS_POOL,\n    }, {\n      serverId: getServerId(),\n      startAtEnd: true,\n      excludedSchemas: {\n        mysql: true,\n      },\n    });\n\n    await instance.start();\n\n    const triggeredEvents = [];\n    instance.addTrigger({\n      name: 'Test',\n      expression: `${TEST_SCHEMA_1}`,\n      statement: MySQLEvents.STATEMENTS.ALL,\n      onEvent: event => triggeredEvents.push(event),\n    });\n\n    await delay(5000);\n\n    await executeQuery(connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);\n    await executeQuery(connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);\n\n    await delay(1000);\n\n    if (!triggeredEvents.length) throw new Error('No trigger was caught');\n    triggeredEvents.splice(0);\n\n    instance.pause();\n    await delay(300);\n\n    await executeQuery(connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test3', 'test4');`);\n    await executeQuery(connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test4', ${TEST_COLUMN_2} = 'test5';`);\n\n    await delay(1000);\n\n    if (triggeredEvents.length) throw new Error('Connection should be stopped');\n\n    instance.resume();\n\n    await delay(1000);\n\n    if (!triggeredEvents.length) throw new Error('No trigger was caught');\n  }, 20000);\n\n});\n"
  }
]