Repository: rodrigogs/mysql-events
Branch: master
Commit: d868f5c36ba3
Files: 24
Total size: 50.5 KB
Directory structure:
gitextract_3xhwks61/
├── .circleci/
│ └── config.yml
├── .codeclimate.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .gitattributes
├── .gitignore
├── .npmignore
├── AUTHORS
├── LICENSE
├── README.md
├── docker-compose.yml
├── examples/
│ └── watchWholeInstance.js
├── index.js
├── lib/
│ ├── EVENTS.enum.js
│ ├── MySQLEvents.js
│ ├── STATEMENTS.enum.js
│ ├── connectionHandler.js
│ ├── dataNormalizer.js
│ ├── eventHandler.js
│ └── index.js
├── package.json
├── scripts/
│ └── test.sh
└── test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
machine: true
working_directory: ~/mysql-events
steps:
- checkout
- restore_cache:
keys:
- lib-dependencies-{{ checksum "package.json" }}
- lib-dependencies-
- run:
name: Setup Code Climate test-reporter
command: |
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
- run:
name: Install dependencies
command: npm install
# - run:
# name: Wait for DB
# command: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
- save_cache:
paths:
- node_modules
key: lib-dependencies-{{ checksum "package.json" }}
- run:
name: Run tests
command: |
chmod +x scripts/test.sh
./scripts/test.sh
# - run:
# name: Run tests/coverage
# command: |
# npm run coverage
# ./cc-test-reporter format-coverage -t lcov -o coverage.json coverage/lcov.info
# ./cc-test-reporter upload-coverage -i coverage.json
================================================
FILE: .codeclimate.yml
================================================
engines:
eslint:
enabled: true
channel: 'eslint-2'
checks:
import/no-unresolved:
enabled: false
new-cap:
enabled: false
ratings:
paths:
- '*.js'
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
charset = utf-8
#trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
================================================
FILE: .eslintrc.yml
================================================
extends: airbnb-base
plugins:
- import
rules:
no-shadow: off
import/no-dynamic-require: off
global-require: off
no-param-reassign: off
consistent-return: off
arrow-body-style: off
no-underscore-dangle: off
import/extensions: off
prefer-destructuring: off
strict: off
max-len: off
no-console: off
no-continue: off
no-return-assign: off
globals:
process: on
describe: on
beforeAll: on
afterAll: on
beforeEach: on
afterEach: on
suite: on
test: on
it: on
================================================
FILE: .gitattributes
================================================
# These settings are for any web project
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
# * text=auto
# 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.
#
# The above will handle all files NOT found below
#
#
## These files are text and should be normalized (Convert crlf => lf)
#
# source code
*.php text
*.css text
*.sass text
*.scss text
*.less text
*.styl text
*.js text
*.coffee text
*.json text
*.htm text
*.html text
*.xml text
*.svg text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
*.sh text
*.bat text
# templates
*.ejs text
*.hbt text
*.jade text
*.haml text
*.hbs text
*.dot text
*.tmpl text
*.phtml text
# server config
.htaccess text
# git config
.gitattributes text
.gitignore text
.gitconfig text
# code analysis config
.jshintrc text
.jscsrc text
.jshintignore text
.csslintrc text
# misc config
*.yaml text
*.yml text
.editorconfig text
# build config
*.npmignore text
*.bowerrc text
# Heroku
Procfile text
.slugignore text
# Documentation
*.md text
LICENSE text
AUTHORS text
#
## These files are binary and should be left untouched
#
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary
================================================
FILE: .gitignore
================================================
/.idea/
/node_modules/
*.iml
/.env
coverage
================================================
FILE: .npmignore
================================================
/.vscode/
/.idea/
/.circleci/
/example/
/examples/
.codeclimate.yml
.editorconfig
.eslintignore
.eslintrc.yml
.gitattributes
.gitignore
example.js
*.iml
================================================
FILE: AUTHORS
================================================
Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com> (https://github.com/rodrigogs)
================================================
FILE: LICENSE
================================================
BSD 3-Clause License
Copyright (c) 2018, Rodrigo Gomes da Silva
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# mysql-events
[](https://circleci.com/gh/rodrigogs/mysql-events)
[](https://codeclimate.com/github/rodrigogs/mysql-events)
[](https://codeclimate.com/github/rodrigogs/mysql-events/coverage)
A [node.js](https://nodejs.org) package that watches a MySQL database and runs callbacks on matched events.
This 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.
Check [@kuroski](https://github.com/kuroski)'s [mysql-events-ui](https://github.com/kuroski/mysql-events-ui) for a `mysql-events` UI implementation.
## Install
```sh
npm install @rodrigogs/mysql-events
```
## Quick Start
```javascript
const mysql = require('mysql');
const MySQLEvents = require('@rodrigogs/mysql-events');
const program = async () => {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
});
const instance = new MySQLEvents(connection, {
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
instance.addTrigger({
name: 'TEST',
expression: '*',
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: (event) => { // You will receive the events here
console.log(event);
},
});
instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);
};
program()
.then(() => console.log('Waiting for database events...'))
.catch(console.error);
```
[Check the examples](https://github.com/rodrigogs/mysql-events/examples)
## Usage
### #constructor(connection, options)
- Instantiate and create a database connection using a DSN
```javascript
const dsn = {
host: 'localhost',
user: 'username',
password: 'password',
};
const myInstance = new MySQLEvents(dsn, { /* ZongJi options */ });
```
- Instantiate and create a database connection using a preexisting connection
```javascript
const connection = mysql.createConnection({
host: 'localhost',
user: 'username',
password: 'password',
});
const myInstance = new MySQLEvents(connection, { /* ZongJi options */ });
```
- Options(the second argument) is for ZongJi options
```javascript
const myInstance = new MySQLEvents({ /* connection */ }, {
serverId: 3,
startAtEnd: true,
});
```
[See more about ZongJi options](https://github.com/rodrigogs/zongji#zongji-class)
### #start()
- start function ensures that MySQL is connected and ZongJi is running before resolving its promise
```javascript
myInstance.start()
.then(() => console.log('I\'m running!'))
.catch(err => console.error('Something bad happened', err));
```
### #stop()
- stop function terminates MySQL connection and stops ZongJi before resolving its promise
```javascript
myInstance.stop()
.then(() => console.log('I\'m stopped!'))
.catch(err => console.error('Something bad happened', err));
```
### #pause()
- 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
```javascript
myInstance.pause();
```
### #resume()
- resume function resumes a paused MySQL connection, so it starts to generate binlog events again
```javascript
myInstance.resume();
```
### #addTrigger({ name, expression, statement, onEvent })
- Adds a trigger for the given expression/statement and calls the `onEvent` function when the event happens
```javascript
instance.addTrigger({
name: 'MY_TRIGGER',
expression: 'MY_SCHEMA.MY_TABLE.MY_COLUMN',
statement: MySQLEvents.STATEMENTS.INSERT,
onEvent: async (event) => {
// Here you will get the events for the given expression/statement.
// This could be an async function.
await doSomething(event);
},
});
```
- The `name` argument must be unique for each expression/statement, it will be user later if you want to remove a trigger
```javascript
instance.addTrigger({
name: 'MY_TRIGGER',
expression: 'MY_SCHEMA.*',
statement: MySQLEvents.STATEMENTS.ALL,
...
});
instance.removeTrigger({
name: 'MY_TRIGGER',
expression: 'MY_SCHEMA.*',
statement: MySQLEvents.STATEMENTS.ALL,
});
```
- The `expression` argument is very dynamic, you can replace any step by `*` to make it wait for any schema, table or column events
```javascript
instance.addTrigger({
name: 'Name updates from table USERS at SCHEMA2',
expression: 'SCHEMA2.USERS.name',
...
});
```
```javascript
instance.addTrigger({
name: 'All database events',
expression: '*',
...
});
```
```javascript
instance.addTrigger({
name: 'All events from SCHEMA2',
expression: 'SCHEMA2.*',
...
});
```
```javascript
instance.addTrigger({
name: 'All database events for table USERS',
expression: '*.USERS',
...
});
```
- The `statement` argument indicates in which database operation an event should be triggered
```javascript
instance.addTrigger({
...
statement: MySQLEvents.STATEMENTS.ALL,
...
});
```
[Allowed statements](https://github.com/rodrigogs/mysql-events/blob/master/lib/STATEMENTS.enum.js)
- The `onEvent` argument is a function where the trigger events should be threated
```javascript
instance.addTrigger({
...
onEvent: (event) => {
console.log(event); // { type, schema, table, affectedRows: [], affectedColumns: [], timestamp, }
},
...
});
```
### #removeTrigger({ name, expression, statement })
- Removes a trigger from the current instance
```javascript
instance.removeTrigger({
name: 'My previous created trigger',
expression: '',
statement: MySQLEvents.STATEMENTS.INSERT,
});
```
### Instance events
- MySQLEvents class emits some events related to its MySQL connection and ZongJi instance
```javascript
instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, (err) => console.log('Connection error', err));
instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, (err) => console.log('ZongJi error', err));
```
[Available events](https://github.com/rodrigogs/mysql-events/blob/master/lib/EVENTS.enum.js)
## Tigger event object
It has the following structure:
```javascript
{
type: 'INSERT | UPDATE | DELETE',
schema: 'SCHEMA_NAME',
table: 'TABLE_NAME',
affectedRows: [{
before: {
column1: 'A',
column2: 'B',
column3: 'C',
...
},
after: {
column1: 'D',
column2: 'E',
column3: 'F',
...
},
}],
affectedColumns: [
'column1',
'column2',
'column3',
],
timestamp: 1530645380029,
nextPosition: 1343,
binlogName: 'bin.001',
}
```
**Make sure the database user has the privilege to read the binlog on database that you want to watch on.**
## LICENSE
[BSD-3-Clause](https://github.com/rodrigogs/mysql-events/blob/master/LICENSE) © Rodrigo Gomes da Silva
================================================
FILE: docker-compose.yml
================================================
---
version: '2'
services:
mysql55:
image: mysql:5.5
container_name: mysql55
command: [ "--server-id=1", "--log-bin=/var/lib/mysql/mysql-bin.log", "--binlog-format=row"]
ports:
- 3355:3306
networks:
default:
aliases:
- mysql55
environment:
MYSQL_ROOT_PASSWORD: root
mysql56:
image: mysql:5.6
container_name: mysql56
command: [ "--server-id=1", "--log-bin=/var/lib/mysql/mysql-bin.log", "--binlog-format=row"]
ports:
- 3356:3306
networks:
default:
aliases:
- mysql56
environment:
MYSQL_ROOT_PASSWORD: root
mysql57:
image: mysql:5.7
container_name: mysql57
command: [ "--server-id=1", "--log-bin=/var/lib/mysql/mysql-bin.log", "--binlog-format=row"]
ports:
- 3357:3306
networks:
default:
aliases:
- mysql57
environment:
MYSQL_ROOT_PASSWORD: root
mysql80:
image: mysql:8.0
container_name: mysql80
command: [ "--server-id=1", "--log-bin=/var/lib/mysql/mysql-bin.log", "--binlog-format=row", "--default-authentication-plugin=mysql_native_password"]
ports:
- 3380:3306
networks:
default:
aliases:
- mysql80
environment:
MYSQL_ROOT_PASSWORD: root
================================================
FILE: examples/watchWholeInstance.js
================================================
const MySQLEvents = require('@rodrigogs/mysql-events');
const program = async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
}, {
startAtEnd: true,
});
await instance.start();
instance.addTrigger({
name: 'Whole database instance',
expression: '*',
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: (event) => {
console.log(event);
},
});
instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);
};
program()
.then(() => console.log('Waiting for database vents...'))
.catch(console.error);
================================================
FILE: index.js
================================================
module.exports = require('./lib');
================================================
FILE: lib/EVENTS.enum.js
================================================
const EVENTS = {
STARTED: 'started',
STOPPED: 'stopped',
PAUSED: 'paused',
RESUMED: 'resumed',
BINLOG: 'binlog',
TRIGGER_ERROR: 'triggerError',
CONNECTION_ERROR: 'connectionError',
ZONGJI_ERROR: 'zongjiError',
};
module.exports = EVENTS;
================================================
FILE: lib/MySQLEvents.js
================================================
const debug = require('debuggler')();
const ZongJi = require('@rodrigogs/zongji');
const EventEmitter = require('events');
const eventHandler = require('./eventHandler');
const connectionHandler = require('./connectionHandler');
const EVENTS = require('./EVENTS.enum');
const STATEMENTS = require('./STATEMENTS.enum');
/**
* @param {Object|Connection|String} connection
* @param {Object} options
*/
class MySQLEvents extends EventEmitter {
constructor(connection, options = {}) {
super();
this.connection = connection;
this.options = options;
this.isStarted = false;
this.isPaused = false;
this.zongJi = null;
this.expressions = {};
}
/**
* @return {{BINLOG, TRIGGER_ERROR, CONNECTION_ERROR, ZONGJI_ERROR}}
* @constructor
*/
static get EVENTS() {
return EVENTS;
}
/**
* @return {{ALL: string, INSERT: string, UPDATE: string, DELETE: string}}
*/
static get STATEMENTS() {
return STATEMENTS;
}
/**
* @param {Object} event binlog event object.
* @private
*/
_handleEvent(event) {
if (!this.zongJi) return;
event.binlogName = this.zongJi.binlogName;
event = eventHandler.normalizeEvent(event);
const triggers = eventHandler.findTriggers(event, this.expressions);
Promise.all(triggers.map(async (trigger) => {
try {
await trigger.onEvent(event);
} catch (error) {
this.emit(EVENTS.TRIGGER_ERROR, { trigger, error });
}
})).then(() => debug('triggers executed'));
}
/**
* @private
*/
_handleZongJiEvents() {
this.zongJi.on('error', err => this.emit(EVENTS.ZONGJI_ERROR, err));
this.zongJi.on('binlog', (event) => {
this.emit(EVENTS.BINLOG, event);
this._handleEvent(event);
});
}
/**
* @private
*/
_handleConnectionEvents() {
this.connection.on('error', err => this.emit(EVENTS.CONNECTION_ERROR, err));
}
/**
* @param {Object} [options = {}]
* @return {Promise<void>}
*/
async start(options = {}) {
if (this.isStarted) return;
debug('connecting to mysql');
this.connection = await connectionHandler(this.connection);
debug('initializing zongji');
this.zongJi = new ZongJi(this.connection, Object.assign({}, this.options, options));
debug('connected');
this.emit('connected');
this._handleConnectionEvents();
this._handleZongJiEvents();
this.zongJi.start(this.options);
this.isStarted = true;
this.emit(EVENTS.STARTED);
}
/**
* @return {Promise<void>}
*/
async stop() {
if (!this.isStarted) return;
debug('disconnecting from mysql');
this.zongJi.stop();
delete this.zongJi;
await new Promise((resolve, reject) => {
this.connection.end((err) => {
if (err) return reject(err);
resolve();
});
});
debug('disconnected');
this.emit('disconnected');
this.isStarted = false;
this.emit(EVENTS.STOPPED);
}
/**
*
*/
pause() {
if (!this.isStarted || this.isPaused) return;
debug('pausing connection');
this.zongJi.connection.pause();
this.isPaused = true;
this.emit(EVENTS.PAUSED);
}
/**
*
*/
resume() {
if (!this.isStarted || !this.isPaused) return;
debug('resuming connection');
this.zongJi.connection.resume();
this.isPaused = false;
this.emit(EVENTS.RESUMED);
}
/**
* @param {String} name
* @param {String} expression
* @param {String} [statement = 'ALL']
* @param {Function} [onEvent]
* @return {void}
*/
addTrigger({
name,
expression,
statement = STATEMENTS.ALL,
onEvent,
}) {
if (!name) throw new Error('Missing trigger name');
if (!expression) throw new Error('Missing trigger expression');
if (typeof onEvent !== 'function') throw new Error('onEvent argument should be a function');
this.expressions[expression] = this.expressions[expression] || {};
this.expressions[expression].statements = this.expressions[expression].statements || {};
this.expressions[expression].statements[statement] = this.expressions[expression].statements[statement] || [];
const triggers = this.expressions[expression].statements[statement];
if (triggers.find(st => st.name === name)) {
throw new Error(`There's already a trigger named "${name}" for expression "${expression}" with statement "${statement}"`);
}
triggers.push({
name,
onEvent,
});
}
/**
* @param {String} name
* @param {String} expression
* @param {String} [statement = 'ALL']
* @return {void}
*/
removeTrigger({
name,
expression,
statement = STATEMENTS.ALL,
}) {
const exp = this.expressions[expression];
if (!exp) return;
const triggers = exp.statements[statement];
if (!triggers) return;
const named = triggers.find(st => st.name === name);
if (!named) return;
const index = triggers.indexOf(named);
triggers.splice(index, 1);
}
}
module.exports = MySQLEvents;
================================================
FILE: lib/STATEMENTS.enum.js
================================================
const STATEMENTS = {
ALL: 'ALL',
INSERT: 'INSERT',
UPDATE: 'UPDATE',
DELETE: 'DELETE',
};
module.exports = STATEMENTS;
================================================
FILE: lib/connectionHandler.js
================================================
const debug = require('debuggler')();
const mysql = require('mysql');
const Connection = require('mysql/lib/Connection');
const Pool = require('mysql/lib/Pool');
const connect = connection => new Promise((resolve, reject) => connection.connect((err) => {
if (err) return reject(err);
resolve();
}));
const connectionHandler = async (connection) => {
if (connection instanceof Pool) {
debug('reusing pool:', connection);
if (connection._closed) {
connection = mysql.createPool(connection.config.connectionConfig);
}
}
if (connection instanceof Connection) {
debug('reusing connection:', connection);
if (connection.state !== 'connected') {
connection = mysql.createConnection(connection.config);
}
}
if (typeof connection === 'string') {
debug('creating connection from string:', connection);
connection = mysql.createConnection(connection);
}
if ((typeof connection === 'object') && (!(connection instanceof Connection) && !(connection instanceof Pool))) {
debug('creating connection from object:', connection);
if (connection.isPool) {
connection = mysql.createPool(connection);
} else {
connection = mysql.createConnection(connection);
}
}
if ((connection instanceof Connection) && (connection.state !== 'connected')) {
debug('initializing connection');
await connect(connection);
}
return connection;
};
module.exports = connectionHandler;
================================================
FILE: lib/dataNormalizer.js
================================================
const STATEMENTS = require('./STATEMENTS.enum');
const getEventType = (eventName) => {
return {
writerows: STATEMENTS.INSERT,
updaterows: STATEMENTS.UPDATE,
deleterows: STATEMENTS.DELETE,
}[eventName];
};
const normalizeRow = (row) => {
if (!row) return undefined;
const columns = Object.getOwnPropertyNames(row);
for (let i = 0, len = columns.length; i < len; i += 1) {
const columnValue = row[columns[i]];
if (columnValue instanceof Buffer && columnValue.length === 1) { // It's a boolean
row[columns[i]] = (columnValue[0] > 0);
}
}
return row;
};
const hasDifference = (beforeValue, afterValue) => {
if ((beforeValue && afterValue) && beforeValue instanceof Date) {
return beforeValue.getTime() !== afterValue.getTime();
}
return beforeValue !== afterValue;
};
const fixRowStructure = (type, row) => {
if (type === STATEMENTS.INSERT) {
row = {
before: undefined,
after: row,
};
}
if (type === STATEMENTS.DELETE) {
row = {
before: row,
after: undefined,
};
}
return row;
};
const resolveAffectedColumns = (normalizedEvent, normalizedRows) => {
const columns = Object.getOwnPropertyNames((normalizedRows.after || normalizedRows.before));
for (let i = 0, len = columns.length; i < len; i += 1) {
const columnName = columns[i];
const beforeValue = (normalizedRows.before || {})[columnName];
const afterValue = (normalizedRows.after || {})[columnName];
if (hasDifference(beforeValue, afterValue)) {
if (normalizedEvent.affectedColumns.indexOf(columnName) === -1) {
normalizedEvent.affectedColumns.push(columnName);
}
}
}
};
const dataNormalizer = (event) => {
const type = getEventType(event.getEventName());
const schema = event.tableMap[event.tableId].parentSchema;
const table = event.tableMap[event.tableId].tableName;
const { timestamp, nextPosition, binlogName } = event;
const normalized = {
type,
schema,
table,
affectedRows: [],
affectedColumns: [],
timestamp,
nextPosition,
binlogName,
};
event.rows.forEach((row) => {
row = fixRowStructure(type, row);
const normalizedRows = {
after: normalizeRow(row.after),
before: normalizeRow(row.before),
};
normalized.affectedRows.push(normalizedRows);
resolveAffectedColumns(normalized, normalizedRows);
});
return normalized;
};
module.exports = dataNormalizer;
================================================
FILE: lib/eventHandler.js
================================================
const normalize = require('./dataNormalizer');
const STATEMENTS = require('./STATEMENTS.enum');
const parseExpression = (expression = '') => {
const parts = expression.split('.');
return {
schema: parts[0],
table: parts[1],
column: parts[2],
value: parts[3],
};
};
const normalizeEvent = (event) => {
const dataEvents = [
'writerows',
'updaterows',
'deleterows',
];
if (dataEvents.indexOf(event.getEventName()) !== -1) {
return normalize(event);
}
return event;
};
/**
* @param {Object} event
* @param {Object} triggers
* @return {Object[]}
*/
const findTriggers = (event, triggers) => {
if (!event.type) return [];
const triggerExpressions = Object.getOwnPropertyNames(triggers);
const statements = [];
for (let i = 0, len = triggerExpressions.length; i < len; i += 1) {
const expression = triggerExpressions[i];
const trigger = triggers[expression];
const parts = parseExpression(expression);
if (parts.schema !== '*' && parts.schema !== event.schema) continue;
if (!(!parts.table || parts.table === '*') && parts.table !== event.table) continue;
if (!(!parts.column || parts.column === '*') && event.affectedColumns.indexOf(parts.column) === -1) continue;
if (trigger.statements[STATEMENTS.ALL]) statements.push(...trigger.statements[STATEMENTS.ALL]);
if (trigger.statements[event.type]) statements.push(...trigger.statements[event.type]);
}
return statements;
};
/**
* @type {{normalizeEvent: normalizeEvent, findTriggers: findTriggers}}
*/
const eventHandler = {
normalizeEvent,
findTriggers,
};
module.exports = eventHandler;
================================================
FILE: lib/index.js
================================================
module.exports = require('./MySQLEvents');
================================================
FILE: package.json
================================================
{
"name": "@rodrigogs/mysql-events",
"version": "0.6.0",
"license": "BSD-3-Clause",
"description": "A node.js package that watches a MySQL database and runs callbacks on matched events like updates on tables and/or specific columns.",
"homepage": "https://github.com/rodrigogs/mysql-events",
"keywords": [
"mysql",
"events",
"trigger",
"notify",
"watcher"
],
"repository": {
"type": "git",
"url": "git@github.com:rodrigogs/mysql-events.git"
},
"main": "index.js",
"scripts": {
"eslint": "eslint . --ext .js",
"test": "npm run test:55 && npm run test:56 && npm run test:57 && npm run test:80",
"test:55": "cross-env DATABASE_PORT=3355 jest --forceExit --runInBand",
"test:56": "cross-env DATABASE_PORT=3356 jest --forceExit --runInBand",
"test:57": "cross-env DATABASE_PORT=3357 jest --forceExit --runInBand",
"test:80": "cross-env DATABASE_PORT=3380 jest --forceExit --runInBand",
"test:local": "./scripts/test.sh",
"coverage": "nyc --reporter=lcov npm test"
},
"dependencies": {
"@rodrigogs/zongji": "^0.4.14",
"debug": "^4.1.1",
"debuggler": "^1.0.0",
"mysql": "^2.17.1"
},
"devDependencies": {
"chai": "^4.2.0",
"codeclimate-test-reporter": "^0.5.1",
"cross-env": "^5.2.0",
"dotenv-cli": "^2.0.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.17.2",
"jest": "^24.8.0",
"nyc": "^14.1.1"
},
"engines": {
"node": ">=7.6.0"
}
}
================================================
FILE: scripts/test.sh
================================================
#!/bin/bash
### CONFIG
export MSYS_NO_PATHCONV=1; # git-bash workaroung for Windows
my_dir="$(dirname "$0")";
### PROGRAM
docker-compose up -d;
echo 'Waiting for docker services...';
while ! docker exec mysql55 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done
while ! docker exec mysql56 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done
while ! docker exec mysql57 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done
while ! docker exec mysql80 mysqladmin ping -h'127.0.0.1' --silent; do sleep 3; done
### SINGLE CONNECTION
docker run --rm -t \
--net=host \
-v `pwd`:/app \
-w /app node:8-alpine \
/bin/sh -c "npm install && npm test"
exitCode=$?;
### CONNECTION POOL
if [ "$exitCode" == "0" ]; then
docker run --rm -t \
--net=host \
-v `pwd`:/app \
-w /app node:8-alpine \
/bin/sh -c "export IS_POOL=true; npm test"
exitCode=$?;
fi
docker-compose down -v;
exit $exitCode;
================================================
FILE: test.js
================================================
/* eslint-disable padded-blocks,no-unused-expressions,no-await-in-loop */
const chai = require('chai');
const mysql = require('mysql');
const MySQLEvents = require('./lib');
const { expect } = chai;
const DATABASE_PORT = process.env.DATABASE_PORT || 3306;
const IS_POOL = process.env.IS_POOL || false;
const TEST_SCHEMA_1 = 'testSchema1';
const TEST_SCHEMA_2 = 'testSchema2';
const TEST_TABLE_1 = 'testTable1';
const TEST_TABLE_2 = 'testTable2';
const TEST_COLUMN_1 = 'column1';
const TEST_COLUMN_2 = 'column2';
const delay = (timeout = 500) => new Promise((resolve) => {
setTimeout(resolve, timeout);
});
let _serverId = 0;
const getServerId = () => {
return _serverId += 1;
};
const getConnection = () => {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
});
return new Promise((resolve, reject) => connection.connect((err) => {
if (err) return reject(err);
resolve(connection);
}));
};
const executeQuery = (conn, query) => {
return new Promise((resolve, reject) => conn.query(query, (err, results) => {
if (err) return reject(err);
resolve(results);
}));
};
const closeConnection = conn => new Promise((resolve, reject) => conn.end((err) => {
if (err) return reject(err);
resolve();
}));
const grantPrivileges = async () => {
const conn = await getConnection();
try {
await executeQuery(conn, 'GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO \'root\'@\'localhost\'');
} catch (err) {
throw err;
} finally {
await closeConnection(conn);
}
};
const createSchemas = async () => {
console.log('Creating connection...');
const conn = await getConnection();
try {
await executeQuery(conn, `CREATE DATABASE IF NOT EXISTS ${TEST_SCHEMA_1};`);
await executeQuery(conn, `CREATE DATABASE IF NOT EXISTS ${TEST_SCHEMA_2};`);
} catch (err) {
throw err;
} finally {
await closeConnection(conn);
}
};
const dropSchemas = async () => {
const conn = await getConnection();
try {
await executeQuery(conn, `DROP DATABASE IF EXISTS ${TEST_SCHEMA_1};`);
await executeQuery(conn, `DROP DATABASE IF EXISTS ${TEST_SCHEMA_2};`);
} catch (err) {
throw err;
} finally {
await closeConnection(conn);
}
};
const createTables = async () => {
const conn = await getConnection();
try {
await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_1} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);
await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_2} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);
await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_1} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);
await executeQuery(conn, `CREATE TABLE IF NOT EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_2} (${TEST_COLUMN_1} varchar(255), ${TEST_COLUMN_2} varchar(255));`);
} catch (err) {
throw err;
} finally {
await closeConnection(conn);
}
};
const dropTables = async () => {
const conn = await getConnection();
try {
await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_1};`);
await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_1}.${TEST_TABLE_2};`);
await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_1};`);
await executeQuery(conn, `DROP TABLE IF EXISTS ${TEST_SCHEMA_2}.${TEST_TABLE_2};`);
} catch (err) {
throw err;
} finally {
await closeConnection(conn);
}
};
beforeAll(async () => {
console.log(`Runnning tests on port ${DATABASE_PORT}...`);
chai.should();
await createSchemas();
await grantPrivileges();
});
beforeEach(async () => {
await createTables();
});
afterEach(async () => {
await dropTables();
});
afterAll(async () => {
await dropSchemas();
});
describe(`MySQLEvents using ${IS_POOL ? 'connection pool' : 'single connection'} on port ${DATABASE_PORT}`, () => {
it('should expose EVENTS enum', async () => {
MySQLEvents.EVENTS.should.be.an('object');
MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('BINLOG');
MySQLEvents.EVENTS.BINLOG.should.be.equal('binlog');
MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('TRIGGER_ERROR');
MySQLEvents.EVENTS.TRIGGER_ERROR.should.be.equal('triggerError');
MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('CONNECTION_ERROR');
MySQLEvents.EVENTS.CONNECTION_ERROR.should.be.equal('connectionError');
MySQLEvents.EVENTS.should.have.ownPropertyDescriptor('ZONGJI_ERROR');
MySQLEvents.EVENTS.ZONGJI_ERROR.should.be.equal('zongjiError');
});
it('should expose STATEMENTS enum', async () => {
MySQLEvents.STATEMENTS.should.be.an('object');
MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('ALL');
MySQLEvents.STATEMENTS.ALL.should.be.equal('ALL');
MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('INSERT');
MySQLEvents.STATEMENTS.INSERT.should.be.equal('INSERT');
MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('UPDATE');
MySQLEvents.STATEMENTS.UPDATE.should.be.equal('UPDATE');
MySQLEvents.STATEMENTS.should.have.ownPropertyDescriptor('DELETE');
MySQLEvents.STATEMENTS.DELETE.should.be.equal('DELETE');
});
it('should connect and disconnect from MySQL using a pre existing connection', async () => {
let connection;
if (IS_POOL) {
connection = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
});
} else {
connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
});
}
const instance = new MySQLEvents(connection);
await instance.start();
await delay();
await instance.stop();
}, 10000);
it('should connect and disconnect from MySQL using a dsn', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
});
await instance.start();
await delay();
await instance.stop();
}, 10000);
it('should connect and disconnect from MySQL using a connection string', async () => {
const instance = new MySQLEvents(`mysql://root:root@localhost:${DATABASE_PORT}/${TEST_SCHEMA_1}`);
await instance.start();
await delay();
await instance.stop();
}, 10000);
it('should catch an event using an INSERT trigger', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggerEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.INSERT,
onEvent: event => triggerEvents.push(event),
});
instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await delay(5000);
if (!triggerEvents.length) throw new Error('No trigger was caught');
triggerEvents[0].should.be.an('object');
triggerEvents[0].should.have.ownPropertyDescriptor('type');
triggerEvents[0].type.should.be.a('string').equals('INSERT');
triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');
triggerEvents[0].timestamp.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('table');
triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);
triggerEvents[0].should.have.ownPropertyDescriptor('schema');
triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);
triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');
triggerEvents[0].nextPosition.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');
triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);
triggerEvents[0].affectedRows[0].should.be.an('object');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');
triggerEvents[0].affectedRows[0].after.should.be.an('object');
triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_1);
triggerEvents[0].affectedRows[0].after[TEST_COLUMN_1].should.be.a('string').equals('test1');
triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_2);
triggerEvents[0].affectedRows[0].after[TEST_COLUMN_2].should.be.a('string').equals('test2');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');
expect(triggerEvents[0].affectedRows[0].before).to.be.an('undefined');
await instance.stop();
}, 15000);
it('should catch an event using an UPDATE trigger', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggerEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.UPDATE,
onEvent: event => triggerEvents.push(event),
});
instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.CONNECTION_ERROR, console.error);
instance.on(MySQLEvents.EVENTS.ZONGJI_ERROR, console.error);
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);
await delay(5000);
if (!triggerEvents.length) throw new Error('No trigger was caught');
triggerEvents[0].should.be.an('object');
triggerEvents[0].should.have.ownPropertyDescriptor('type');
triggerEvents[0].type.should.be.a('string').equals('UPDATE');
triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');
triggerEvents[0].timestamp.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('table');
triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);
triggerEvents[0].should.have.ownPropertyDescriptor('schema');
triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);
triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');
triggerEvents[0].nextPosition.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');
triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);
triggerEvents[0].affectedRows[0].should.be.an('object');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');
triggerEvents[0].affectedRows[0].after.should.be.an('object');
triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_1);
triggerEvents[0].affectedRows[0].after[TEST_COLUMN_1].should.be.a('string').equals('test3');
triggerEvents[0].affectedRows[0].after.should.have.ownPropertyDescriptor(TEST_COLUMN_2);
triggerEvents[0].affectedRows[0].after[TEST_COLUMN_2].should.be.a('string').equals('test4');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');
triggerEvents[0].affectedRows[0].before.should.be.an('object');
triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_1);
triggerEvents[0].affectedRows[0].before[TEST_COLUMN_1].should.be.a('string').equals('test1');
triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_2);
triggerEvents[0].affectedRows[0].before[TEST_COLUMN_2].should.be.a('string').equals('test2');
await instance.stop();
}, 15000);
it('should catch an event using a DELETE trigger', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggerEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.DELETE,
onEvent: event => triggerEvents.push(event),
});
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(instance.connection, `DELETE FROM ${TEST_SCHEMA_1}.${TEST_TABLE_1} WHERE ${TEST_COLUMN_1} = 'test1' AND ${TEST_COLUMN_2} = 'test2';`);
await delay(5000);
if (!triggerEvents.length) throw new Error('No trigger was caught');
triggerEvents[0].should.be.an('object');
triggerEvents[0].should.have.ownPropertyDescriptor('type');
triggerEvents[0].type.should.be.a('string').equals('DELETE');
triggerEvents[0].should.have.ownPropertyDescriptor('timestamp');
triggerEvents[0].timestamp.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('table');
triggerEvents[0].table.should.be.a('string').equals(TEST_TABLE_1);
triggerEvents[0].should.have.ownPropertyDescriptor('schema');
triggerEvents[0].schema.should.be.a('string').equals(TEST_SCHEMA_1);
triggerEvents[0].should.have.ownPropertyDescriptor('nextPosition');
triggerEvents[0].nextPosition.should.be.a('number');
triggerEvents[0].should.have.ownPropertyDescriptor('affectedRows');
triggerEvents[0].affectedRows.should.be.an('array').to.have.lengthOf(1);
triggerEvents[0].affectedRows[0].should.be.an('object');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('after');
expect(triggerEvents[0].affectedRows[0].after).to.be.an('undefined');
triggerEvents[0].affectedRows[0].should.have.ownPropertyDescriptor('before');
triggerEvents[0].affectedRows[0].before.should.be.an('object');
triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_1);
triggerEvents[0].affectedRows[0].before[TEST_COLUMN_1].should.be.a('string').equals('test1');
triggerEvents[0].affectedRows[0].before.should.have.ownPropertyDescriptor(TEST_COLUMN_2);
triggerEvents[0].affectedRows[0].before[TEST_COLUMN_2].should.be.a('string').equals('test2');
await instance.stop();
}, 15000);
it('should catch events using an ALL trigger', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggerEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: event => triggerEvents.push(event),
});
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);
await executeQuery(instance.connection, `DELETE FROM ${TEST_SCHEMA_1}.${TEST_TABLE_1} WHERE ${TEST_COLUMN_1} = 'test3' AND ${TEST_COLUMN_2} = 'test4';`);
await delay(1000);
expect(triggerEvents).to.be.an('array').that.is.not.empty;
triggerEvents[0].should.have.ownPropertyDescriptor('type');
triggerEvents[0].type.should.be.a('string').equals('INSERT');
triggerEvents[1].should.have.ownPropertyDescriptor('type');
triggerEvents[1].type.should.be.a('string').equals('UPDATE');
triggerEvents[2].should.have.ownPropertyDescriptor('type');
triggerEvents[2].type.should.be.a('string').equals('DELETE');
await instance.stop();
}, 15000);
it('should remove a previously added event trigger', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
});
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: () => {},
});
instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL].should.be.an('array').that.is.not.empty;
instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].should.be.an('object');
instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].name.should.be.a('string').equals('Test');
instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0].onEvent.should.be.a('function');
instance.removeTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
});
expect(instance.expressions[`${TEST_SCHEMA_1}.${TEST_TABLE_1}`].statements[MySQLEvents.STATEMENTS.ALL][0]).to.be.an('undefined');
await instance.stop();
}, 10000);
it('should throw an error when adding duplicated trigger name for a statement', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
});
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: () => {},
});
expect(() => instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: () => {},
})).to.throw(Error);
});
it('should emit an event when a trigger produces an error', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
await delay();
let error = null;
instance.on(MySQLEvents.EVENTS.TRIGGER_ERROR, (err) => {
error = err;
});
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}.${TEST_TABLE_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: () => {
throw new Error('Error');
},
});
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await delay(1000);
expect(error).to.be.an('object');
error.trigger.should.be.an('object');
error.error.should.be.an('Error');
}, 10000);
it('should receive events from multiple schemas', async () => {
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggeredEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}`,
statement: MySQLEvents.STATEMENTS.UPDATE,
onEvent: event => triggeredEvents.push(event),
});
instance.addTrigger({
name: 'Test2',
expression: `${TEST_SCHEMA_2}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: event => triggeredEvents.push(event),
});
await delay(5000);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);
await executeQuery(instance.connection, `INSERT INTO ${TEST_SCHEMA_2}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(instance.connection, `UPDATE ${TEST_SCHEMA_2}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);
await delay(1000);
if (!triggeredEvents.length) throw new Error('No trigger was caught');
}, 20000);
it('should pause and resume connection', async () => {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
});
const instance = new MySQLEvents({
host: 'localhost',
user: 'root',
password: 'root',
port: DATABASE_PORT,
isPool: IS_POOL,
}, {
serverId: getServerId(),
startAtEnd: true,
excludedSchemas: {
mysql: true,
},
});
await instance.start();
const triggeredEvents = [];
instance.addTrigger({
name: 'Test',
expression: `${TEST_SCHEMA_1}`,
statement: MySQLEvents.STATEMENTS.ALL,
onEvent: event => triggeredEvents.push(event),
});
await delay(5000);
await executeQuery(connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test1', 'test2');`);
await executeQuery(connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test3', ${TEST_COLUMN_2} = 'test4';`);
await delay(1000);
if (!triggeredEvents.length) throw new Error('No trigger was caught');
triggeredEvents.splice(0);
instance.pause();
await delay(300);
await executeQuery(connection, `INSERT INTO ${TEST_SCHEMA_1}.${TEST_TABLE_1} VALUES ('test3', 'test4');`);
await executeQuery(connection, `UPDATE ${TEST_SCHEMA_1}.${TEST_TABLE_1} SET ${TEST_COLUMN_1} = 'test4', ${TEST_COLUMN_2} = 'test5';`);
await delay(1000);
if (triggeredEvents.length) throw new Error('Connection should be stopped');
instance.resume();
await delay(1000);
if (!triggeredEvents.length) throw new Error('No trigger was caught');
}, 20000);
});
gitextract_3xhwks61/ ├── .circleci/ │ └── config.yml ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .npmignore ├── AUTHORS ├── LICENSE ├── README.md ├── docker-compose.yml ├── examples/ │ └── watchWholeInstance.js ├── index.js ├── lib/ │ ├── EVENTS.enum.js │ ├── MySQLEvents.js │ ├── STATEMENTS.enum.js │ ├── connectionHandler.js │ ├── dataNormalizer.js │ ├── eventHandler.js │ └── index.js ├── package.json ├── scripts/ │ └── test.sh └── test.js
SYMBOL INDEX (27 symbols across 6 files)
FILE: lib/EVENTS.enum.js
constant EVENTS (line 1) | const EVENTS = {
FILE: lib/MySQLEvents.js
constant EVENTS (line 7) | const EVENTS = require('./EVENTS.enum');
constant STATEMENTS (line 8) | const STATEMENTS = require('./STATEMENTS.enum');
class MySQLEvents (line 14) | class MySQLEvents extends EventEmitter {
method constructor (line 15) | constructor(connection, options = {}) {
method EVENTS (line 32) | static get EVENTS() {
method STATEMENTS (line 39) | static get STATEMENTS() {
method _handleEvent (line 47) | _handleEvent(event) {
method _handleZongJiEvents (line 66) | _handleZongJiEvents() {
method _handleConnectionEvents (line 77) | _handleConnectionEvents() {
method start (line 85) | async start(options = {}) {
method stop (line 105) | async stop() {
method pause (line 128) | pause() {
method resume (line 140) | resume() {
method addTrigger (line 156) | addTrigger({
method removeTrigger (line 187) | removeTrigger({
FILE: lib/STATEMENTS.enum.js
constant STATEMENTS (line 1) | const STATEMENTS = {
FILE: lib/dataNormalizer.js
constant STATEMENTS (line 1) | const STATEMENTS = require('./STATEMENTS.enum');
FILE: lib/eventHandler.js
constant STATEMENTS (line 2) | const STATEMENTS = require('./STATEMENTS.enum');
FILE: test.js
constant DATABASE_PORT (line 9) | const DATABASE_PORT = process.env.DATABASE_PORT || 3306;
constant IS_POOL (line 10) | const IS_POOL = process.env.IS_POOL || false;
constant TEST_SCHEMA_1 (line 11) | const TEST_SCHEMA_1 = 'testSchema1';
constant TEST_SCHEMA_2 (line 12) | const TEST_SCHEMA_2 = 'testSchema2';
constant TEST_TABLE_1 (line 13) | const TEST_TABLE_1 = 'testTable1';
constant TEST_TABLE_2 (line 14) | const TEST_TABLE_2 = 'testTable2';
constant TEST_COLUMN_1 (line 15) | const TEST_COLUMN_1 = 'column1';
constant TEST_COLUMN_2 (line 16) | const TEST_COLUMN_2 = 'column2';
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
{
"path": ".circleci/config.yml",
"chars": 1201,
"preview": "version: 2\njobs:\n build:\n machine: true\n working_directory: ~/mysql-events\n steps:\n - checkout\n\n - r"
},
{
"path": ".codeclimate.yml",
"chars": 191,
"preview": "engines:\n eslint:\n enabled: true\n channel: 'eslint-2'\n checks:\n import/no-unresolved:\n enabled: fa"
},
{
"path": ".editorconfig",
"chars": 189,
"preview": "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\n#trim_trailing_whitespace = true\ninsert_final_newline = true\nindent_st"
},
{
"path": ".eslintignore",
"chars": 0,
"preview": ""
},
{
"path": ".eslintrc.yml",
"chars": 499,
"preview": "extends: airbnb-base\nplugins:\n - import\nrules:\n no-shadow: off\n import/no-dynamic-require: off\n global-require: off\n"
},
{
"path": ".gitattributes",
"chars": 1806,
"preview": "# These settings are for any web project\n\n# Handle line endings automatically for files detected as text\n# and leave all"
},
{
"path": ".gitignore",
"chars": 44,
"preview": "/.idea/\n/node_modules/\n*.iml\n/.env\ncoverage\n"
},
{
"path": ".npmignore",
"chars": 153,
"preview": "/.vscode/\n/.idea/\n/.circleci/\n/example/\n/examples/\n.codeclimate.yml\n.editorconfig\n.eslintignore\n.eslintrc.yml\n.gitattrib"
},
{
"path": "AUTHORS",
"chars": 81,
"preview": "Rodrigo Gomes da Silva <rodrigo.smscom@gmail.com> (https://github.com/rodrigogs)\n"
},
{
"path": "LICENSE",
"chars": 1522,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2018, Rodrigo Gomes da Silva\nAll rights reserved.\n\nRedistribution and use in source "
},
{
"path": "README.md",
"chars": 7629,
"preview": "# mysql-events\n[](https://circleci.com/gh/rodrigogs/mysql"
},
{
"path": "docker-compose.yml",
"chars": 1291,
"preview": "---\nversion: '2'\nservices:\n mysql55:\n image: mysql:5.5\n container_name: mysql55\n command: [ \"--server-id=1\", \""
},
{
"path": "examples/watchWholeInstance.js",
"chars": 670,
"preview": "const MySQLEvents = require('@rodrigogs/mysql-events');\n\nconst program = async () => {\n const instance = new MySQLEvent"
},
{
"path": "index.js",
"chars": 35,
"preview": "module.exports = require('./lib');\n"
},
{
"path": "lib/EVENTS.enum.js",
"chars": 255,
"preview": "const EVENTS = {\n STARTED: 'started',\n STOPPED: 'stopped',\n PAUSED: 'paused',\n RESUMED: 'resumed',\n BINLOG: 'binlog"
},
{
"path": "lib/MySQLEvents.js",
"chars": 5003,
"preview": "const debug = require('debuggler')();\nconst ZongJi = require('@rodrigogs/zongji');\nconst EventEmitter = require('events'"
},
{
"path": "lib/STATEMENTS.enum.js",
"chars": 128,
"preview": "const STATEMENTS = {\n ALL: 'ALL',\n INSERT: 'INSERT',\n UPDATE: 'UPDATE',\n DELETE: 'DELETE',\n};\n\nmodule.exports = STAT"
},
{
"path": "lib/connectionHandler.js",
"chars": 1457,
"preview": "const debug = require('debuggler')();\nconst mysql = require('mysql');\nconst Connection = require('mysql/lib/Connection')"
},
{
"path": "lib/dataNormalizer.js",
"chars": 2463,
"preview": "const STATEMENTS = require('./STATEMENTS.enum');\n\nconst getEventType = (eventName) => {\n return {\n writerows: STATEM"
},
{
"path": "lib/eventHandler.js",
"chars": 1646,
"preview": "const normalize = require('./dataNormalizer');\nconst STATEMENTS = require('./STATEMENTS.enum');\n\nconst parseExpression ="
},
{
"path": "lib/index.js",
"chars": 43,
"preview": "module.exports = require('./MySQLEvents');\n"
},
{
"path": "package.json",
"chars": 1523,
"preview": "{\n \"name\": \"@rodrigogs/mysql-events\",\n \"version\": \"0.6.0\",\n \"license\": \"BSD-3-Clause\",\n \"description\": \"A node.js pa"
},
{
"path": "scripts/test.sh",
"chars": 931,
"preview": "#!/bin/bash\n\n### CONFIG\nexport MSYS_NO_PATHCONV=1; # git-bash workaroung for Windows\n\nmy_dir=\"$(dirname \"$0\")\";\n\n### PRO"
},
{
"path": "test.js",
"chars": 22949,
"preview": "/* eslint-disable padded-blocks,no-unused-expressions,no-await-in-loop */\n\nconst chai = require('chai');\nconst mysql = r"
}
]
About this extraction
This page contains the full source code of the rodrigogs/mysql-events GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (50.5 KB), approximately 13.7k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.