├── .npmignore ├── circle.yml ├── scripts ├── watch.sh ├── build.sh ├── dmn.sh ├── doctoc.sh └── release.sh ├── src ├── index.js ├── index.test.js ├── add-hooks.js ├── integration.test.js └── add-hooks.test.js ├── .gitignore ├── CHANGELOG.md ├── package.json ├── README.md ├── .eslintrc └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | 3 | 4 | .DS_Store 5 | .eslintrc 6 | .git* 7 | .npmignore 8 | 9 | 10 | CHANGELOG.* -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.6.0 4 | 5 | dependencies: 6 | pre: 7 | - npm prune 8 | post: 9 | - mkdir -p $CIRCLE_TEST_REPORTS/ava 10 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | babel-watch src/index.js 7 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | npm run -s doctoc 7 | NODE_ENV=production babel src --out-dir dist --ignore '**/*.test.js' 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import addHooks from './add-hooks.js' 2 | import {Readable} from 'stream' 3 | 4 | export default (sequelize) => { 5 | const stream = new Readable({ 6 | objectMode: true 7 | , read () {} 8 | }) 9 | addHooks({sequelize, stream}) 10 | return stream 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.patch 10 | *.swp 11 | .DS_store 12 | pids 13 | logs 14 | results 15 | npm-debug.log 16 | node_modules 17 | .yo-rc.json 18 | .cache* 19 | dist/ 20 | .eslintcache 21 | .sublime-project 22 | .sublime-workspace 23 | -------------------------------------------------------------------------------- /scripts/dmn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | function commit_npmignore () { 7 | if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != '' ]]; then 8 | git add .npmignore && \ 9 | git commit --no-verify -m'Chore update npmignore' 10 | fi 11 | } 12 | 13 | dmn gen -f . && commit_npmignore 14 | 15 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import sequelizeStream from './index.js' 3 | import Seqeulize from 'sequelize' 4 | import {Readable} from 'stream' 5 | 6 | test.beforeEach((t) => { 7 | t.context.sequelize = new Seqeulize({ 8 | dialect: 'sqlite' 9 | , logging: false 10 | }) 11 | }) 12 | 13 | test('returns a readable stream', (t) => { 14 | const {sequelize} = t.context 15 | const stream = sequelizeStream(sequelize) 16 | t.truthy(stream instanceof Readable) 17 | }) 18 | -------------------------------------------------------------------------------- /scripts/doctoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | function commit_changes () { 7 | if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != '' ]]; then 8 | ( test -f README.md && git add README.md ) || \ 9 | ( test -f CONTRIBUTING.md && git add CONTRIBUTING.md ) && \ 10 | git commit --no-verify -m'Docs table of contents update' 11 | fi 12 | } 13 | 14 | if [ -f README.md ]; then 15 | doctoc --maxlevel 2 --title '## Contents' README.md 16 | fi 17 | if [ -f CONTRIBUTING.md ]; then 18 | doctoc --maxlevel 2 --title '## Contents' CONTRIBUTING.md 19 | fi 20 | commit_changes 21 | 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.2 | 2016-11-30 4 | Adds integration tests and doc improvements. Nothing user facing. 5 | 6 | * Fix(scripts) doctoc only finds present files 7 | * Docs table of contents update 8 | * Docs add note about bulkUpdate caveat 9 | * Docs fix example typos 10 | * Docs #typo 11 | * Test install eslint-ava 12 | * Test add integration tests 13 | * Chore(build) add yarn.lock 14 | * Chore(deps) update eslint-plugin-ava [major] 15 | * Chore(deps) update patches and minors 16 | * Chore add repository to package.json 17 | 18 | ## v1.0.1 | 2016-10-03 19 | * I typo'd the package name. #oops 20 | * Doc typos fixed 21 | 22 | ## v1.0.0 | 2016-09-24 23 | Release as 1.0! 24 | 25 | 26 | ## v0.0.2 | 2016-09-24 27 | Init 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/add-hooks.js: -------------------------------------------------------------------------------- 1 | const PREFIX = 'sequelize-stream' 2 | const EVENTS = { 3 | CREATE: 'create' 4 | , UPDATE: 'update' 5 | , DESTROY: 'destroy' 6 | // unsupported, just here so we can provide a test case and be alerted if 7 | // sequelize fixes this in the future. 8 | , BULK_DESTROY: 'bulk destroy' 9 | } 10 | 11 | export default ({sequelize, stream}) => { 12 | sequelize.addHook('afterCreate', `${PREFIX}-afterCreate`, (instance) => { 13 | stream.push({event: EVENTS.CREATE, instance}) 14 | }) 15 | 16 | sequelize.addHook('afterBulkCreate', `${PREFIX}-afterBulkCreate`, (instances) => { 17 | instances.forEach((instance) => { 18 | stream.push({event: EVENTS.CREATE, instance}) 19 | }) 20 | }) 21 | 22 | sequelize.addHook('afterUpdate', `${PREFIX}-afterUpdate`, (instance) => { 23 | stream.push({event: EVENTS.UPDATE, instance}) 24 | }) 25 | 26 | sequelize.addHook('afterBulkUpdate', `${PREFIX}-afterBulkUpdate`, ({model, attributes}) => { 27 | // this is a hacky way to get the updated rows 28 | const {updatedAt} = attributes 29 | return model.findAll({where: {updatedAt}}) 30 | .then((instances) => { 31 | instances.forEach((instance) => { 32 | stream.push({event: EVENTS.UPDATE, instance}) 33 | }) 34 | }) 35 | }) 36 | 37 | sequelize.addHook('afterDestroy', `${PREFIX}-afterDestroy`, (instance) => { 38 | stream.push({event: EVENTS.DESTROY, instance}) 39 | }) 40 | 41 | // sequelize doesn't pass the instances to us, so all we can do is emit a 42 | // destroy event 43 | sequelize.addHook('afterBulkDestroy', `${PREFIX}-afterBulkDestroy`, () => { 44 | stream.push({event: EVENTS.BULK_DESTROY}) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-stream", 3 | "version": "1.0.2", 4 | "description": "Create a stream of Sequelize create, update, and destroy events.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "ava --tap=${CI-false} | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)", 8 | "tdd": "ava --watch", 9 | "watch": "SCRIPTY_SILENT=true scripty", 10 | "build": "SCRIPTY_SILENT=true scripty", 11 | "doctoc": "SCRIPTY_SILENT=true scripty", 12 | "dmn": "SCRIPTY_SILENT=true scripty", 13 | "release": "SCRIPTY_SILENT=true scripty", 14 | "lint": "eslint src $@", 15 | "prepublish": "npm run -s build" 16 | }, 17 | "keywords": [ 18 | "sequelize", 19 | "postgres", 20 | "sqlite", 21 | "mysql", 22 | "events", 23 | "real-time", 24 | "CRUD" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/joeybaker/sequelize-stream.git" 29 | }, 30 | "author": "Joey Baker (https://byjoeybaker.com)", 31 | "license": "Artistic-2.0", 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "dependencies": {}, 36 | "peerDependencies": { 37 | "sequelize": ">=4.2.0" 38 | }, 39 | "devDependencies": { 40 | "ava": "^0.24.0", 41 | "babel-cli": "^6.18.0", 42 | "babel-eslint": "^8.0.0", 43 | "babel-plugin-add-module-exports": "^0.2.1", 44 | "babel-plugin-transform-object-rest-spread": "^6.19.0", 45 | "babel-preset-latest": "^6.16.0", 46 | "babel-register": "^6.18.0", 47 | "babel-watch": "^2.0.3", 48 | "dmn": "^1.0.10", 49 | "doctoc": "^1.2.0", 50 | "eslint": "^5.0.0", 51 | "eslint-plugin-ava": "^4.0.0", 52 | "eslint-plugin-filenames": "^1.1.0", 53 | "eslint-plugin-lean-imports": "^0.3.3", 54 | "scripty": "^1.6.0", 55 | "sequelize": "^5.1.0", 56 | "sqlite3": "^4.0.0", 57 | "tap-xunit": "^2.0.0" 58 | }, 59 | "babel": { 60 | "presets": [ 61 | "latest" 62 | ], 63 | "plugins": [ 64 | "add-module-exports", 65 | "transform-object-rest-spread" 66 | ], 67 | "sourceMaps": "inline" 68 | }, 69 | "ava": { 70 | "source": [ 71 | "!dist/**", 72 | "**/*.js" 73 | ], 74 | "files": [ 75 | "**/*.test.js" 76 | ], 77 | "require": [ 78 | "babel-register" 79 | ], 80 | "babel": "inherit" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/integration.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import Sequelize from 'sequelize' 3 | import sequelizeStream from './index.js' 4 | 5 | test.beforeEach((t) => { 6 | const sequelize = t.context.squelize = new Sequelize({ 7 | dialect: 'sqlite' 8 | , logging: false 9 | }) 10 | 11 | t.context.Cat = sequelize.define('Cat', { 12 | name: Sequelize.STRING 13 | , spots: Sequelize.INTEGER 14 | }) 15 | 16 | t.context.stream = sequelizeStream(sequelize) 17 | return sequelize.sync({force: true}) 18 | }) 19 | 20 | test.cb('update has previous values', (t) => { 21 | const {stream, Cat} = t.context 22 | 23 | 24 | stream.on('data', ({instance, event}) => { 25 | if (event === 'update') { 26 | t.truthy(instance.changed('spots')) 27 | t.end() 28 | } 29 | }) 30 | 31 | Cat.create({name: 'spot'}).then((spot) => spot.update({spots: 1})) 32 | }) 33 | 34 | // sequelize needs to update it's bulkUpdate algorithim to accomodate this 35 | test.cb.failing(`bulkUpdate has previous values won't work b/c the instances are fresh.`, (t) => { 36 | const {stream, Cat} = t.context 37 | 38 | 39 | stream.on('data', ({instance, event}) => { 40 | if (event === 'update') { 41 | t.truthy(instance.changed('spots')) 42 | t.end() 43 | } 44 | }) 45 | 46 | Cat.create({name: 'spot'}).then(() => { 47 | return Cat.update({spots: 1}, {where: {name: 'spot'}}) 48 | }) 49 | }) 50 | 51 | test.cb(`bulk destroy with \`individualHooks\``, (t) => { 52 | const {stream, Cat} = t.context 53 | const models = [{name: 'spot'}, {name: 'tails'}] 54 | let destroyCount = 0 55 | 56 | stream.on('data', ({instance, event}) => { 57 | if (event === 'destroy') { 58 | destroyCount++ 59 | t.pass(`${instance.name} deleted`) 60 | 61 | if (destroyCount === models.length) t.end() 62 | } 63 | }) 64 | 65 | Cat.bulkCreate(models).then(() => { 66 | return Cat.destroy({where: {name: {$ne: null}}, individualHooks: true}) 67 | }) 68 | }) 69 | 70 | test.cb.failing(`bulk destroy has doesn't work because sequelize doesn't give us a way to grab the instances`, (t) => { 71 | const {stream, Cat} = t.context 72 | const models = [{name: 'spot'}, {name: 'tails'}] 73 | let destroyCount = 0 74 | 75 | stream.on('data', ({instance, event}) => { 76 | if (event === 'bulk destroy') { 77 | t.fail(`bulk destroy event only deleted`) 78 | t.end() 79 | } 80 | else if (event === 'destroy') { 81 | destroyCount++ 82 | t.pass(`${instance.name} deleted`) 83 | 84 | if (destroyCount === models.length) t.end() 85 | } 86 | }) 87 | 88 | Cat.bulkCreate(models).then(() => { 89 | return Cat.destroy({where: {name: {$ne: null}}}) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /src/add-hooks.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import addHooks from './add-hooks.js' 3 | import Seqeulize from 'sequelize' 4 | import {Readable} from 'stream' 5 | 6 | test.beforeEach((t) => { 7 | const sequelize = t.context.sequelize = new Seqeulize({ 8 | dialect: 'sqlite' 9 | , logging: false 10 | }) 11 | 12 | t.context.model = sequelize.define('cat', { 13 | name: Seqeulize.STRING 14 | }) 15 | 16 | const stream = t.context.stream = new Readable({ 17 | read () { 18 | } 19 | , objectMode: true 20 | }) 21 | 22 | addHooks({sequelize, stream}) 23 | 24 | return sequelize.sync({force: true}) 25 | }) 26 | 27 | test('afterCreate', (t) => { 28 | t.plan(2) 29 | 30 | const {model, stream} = t.context 31 | const name = 'foo' 32 | 33 | stream.on('data', ({instance, event}) => { 34 | t.is(event, 'create') 35 | t.is(instance.name, name) 36 | }) 37 | 38 | return model.create({name}) 39 | }) 40 | 41 | test('afterUpdate', (t) => { 42 | t.plan(2) 43 | 44 | const {model, stream} = t.context 45 | const originalName = 'foo' 46 | const updateName = 'bar' 47 | 48 | stream.on('data', ({instance, event}) => { 49 | if (event === 'create') return 50 | 51 | t.is(event, 'update') 52 | t.is(instance.name, updateName) 53 | }) 54 | 55 | return model.create({name: originalName}) 56 | .then((instance) => instance.update({name: updateName})) 57 | }) 58 | 59 | 60 | test('afterDestroy', (t) => { 61 | t.plan(2) 62 | 63 | const {model, stream} = t.context 64 | const name = 'foo' 65 | 66 | stream.on('data', ({instance, event}) => { 67 | if (event === 'create') return 68 | 69 | t.is(event, 'destroy') 70 | t.is(instance.name, name) 71 | }) 72 | 73 | return model.create({name}) 74 | .then((instance) => instance.destroy()) 75 | }) 76 | 77 | test('afterBulkCreate', (t) => { 78 | const {model, stream} = t.context 79 | const name1 = 'foo' 80 | const name2 = 'bar' 81 | const names = [name1, name2] 82 | const ASSERTS = 2 83 | 84 | t.plan(names.length * ASSERTS) 85 | 86 | stream.on('data', ({instance, event}) => { 87 | t.is(event, 'create') 88 | t.truthy(names.includes(instance.name)) 89 | }) 90 | 91 | return model.bulkCreate([{name: name1}, {name: name2}]) 92 | }) 93 | 94 | test('afterBulkUpdate', (t) => { 95 | const {model, stream} = t.context 96 | const name1 = 'foo' 97 | const name2 = 'bar' 98 | const originalNames = [name1, name2] 99 | const newName = 'baz' 100 | const ASSERTS = 2 101 | 102 | t.plan(originalNames.length * ASSERTS) 103 | 104 | stream.on('data', ({instance, event}) => { 105 | if (event === 'create') return 106 | t.is(event, 'update') 107 | t.is(instance.name, newName) 108 | }) 109 | 110 | return model.bulkCreate([{name: name1}, {name: name2}]) 111 | .then(() => model.update({name: newName}, {where: {name: {$ne: newName}}})) 112 | }) 113 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | function git_require_clean_work_tree(){ 7 | git diff --exit-code 8 | } 9 | 10 | function find_changelog_file(){ 11 | # find the changelog file 12 | local CHANGELOG="" 13 | if test "$CHANGELOG" = ""; then 14 | CHANGELOG="$(ls | egrep '^(change|history)' -i | head -n1)" 15 | if test "$CHANGELOG" = ""; then 16 | CHANGELOG="CHANGELOG.md"; 17 | fi 18 | fi 19 | echo $CHANGELOG 20 | } 21 | 22 | function find_last_git_tag(){ 23 | node -pe "a=$(npm version); 'v' + a[require('./package.json').name]" 24 | } 25 | 26 | # based on https://github.com/tj/git-extras/blob/master/bin/git-changelog 27 | function generate_git_changelog(){ 28 | GIT_LOG_OPTS="--no-merges" 29 | local DATE 30 | DATE=$(date +'%Y-%m-%d') 31 | local HEAD='## ' 32 | 33 | # get the commits between the most recent tag and the second most recent 34 | local lasttag 35 | lasttag=$(find_last_git_tag) 36 | local version 37 | version=$(git describe --tags --abbrev=0 "$lasttag" 2>/dev/null) 38 | local previous_version 39 | previous_version=$(git describe --tags --abbrev=0 "$lasttag^" 2>/dev/null) 40 | # if we don't have a previous version to look at 41 | if test -z "$version"; then 42 | local head="$HEAD$DATE" 43 | local changes 44 | changes=$(git log $GIT_LOG_OPTS --pretty="format:* %s%n" 2>/dev/null) 45 | # the more common case, there's a version to git the changes betwen 46 | else 47 | local head="$HEAD$version | $DATE" 48 | # tail to get remove the first line, which will always just be the version commit 49 | # awk to remove empty lines 50 | local changes 51 | changes=$(tail -n +2 <<< "$(git log $GIT_LOG_OPTS --pretty="format:* %s%n" "$previous_version..$version" 2>/dev/null)" | awk NF) 52 | fi 53 | 54 | local CHANGELOG 55 | CHANGELOG=$(find_changelog_file) 56 | 57 | echo "Editing $CHANGELOG" 58 | # insert the changes after the header (assumes markdown) 59 | # this shells out to node b/c I couldn't figure out how to do it with awk 60 | local tmp_changelog=/tmp/changelog 61 | node -e "console.log(require('fs').readFileSync(process.argv[1]).toString().replace(/(#.*?\n\n)/, '\$1' + process.argv.slice(2).join('\n') + '\n\n'))" "$CHANGELOG" "$head" "$changes" > $tmp_changelog 62 | 63 | # open the changelog in the editor for editing 64 | ${EDITOR:-'vi'} $tmp_changelog 65 | mv $tmp_changelog "$CHANGELOG" 66 | } 67 | 68 | function git_ammend_tag(){ 69 | local changelog_file 70 | local changes 71 | changes=$(git diff --minimal --diff-filter=M --unified=0 --color=never "$changelog_file" | grep '^\+' | egrep -v '^\+\+' | cut -c 2-) 72 | changelog_file="$(find_changelog_file)" 73 | 74 | git add "$changelog_file" 75 | git commit --amend --no-edit --no-verify 76 | git tag "$(find_last_git_tag)" -f -a -m "$changes" 77 | } 78 | 79 | function npm_release(){ 80 | local version="${1-patch}" 81 | 82 | npm version "$version" && \ 83 | generate_git_changelog && \ 84 | git_ammend_tag && \ 85 | git push --follow-tags --no-verify && git push --tags --no-verify && \ 86 | npm publish 87 | } 88 | 89 | git_require_clean_work_tree && \ 90 | git pull --rebase origin master && \ 91 | npm run -s dmn && \ 92 | npm run -s lint && \ 93 | npm test && \ 94 | npm_release $@ 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-stream [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-url]][daviddm-image] [![Build Status][circleci-image]][circleci-url] 2 | 3 | Create a stream of [Sequelize](http://sequelizejs.com) create, update, and destroy events. This is useful if you want to build a real-time stream of events in your database. 4 | 5 | 6 | 7 | ## Contents 8 | 9 | - [Install](#install) 10 | - [Usage](#usage) 11 | - [Methods](#methods) 12 | - [Events](#events) 13 | - [Caveats](#caveats) 14 | - [Tests](#tests) 15 | - [Developing](#developing) 16 | - [License](#license) 17 | 18 | 19 | 20 | 21 | ## Install 22 | 23 | ```sh 24 | npm i -S sequelize-stream 25 | ``` 26 | 27 | 28 | ## Usage 29 | 30 | ```js 31 | // setup sequelize 32 | import Sequelize from 'sequelize' 33 | const sequelize = new Sequelize({ dialect: 'sqlite' }) 34 | const Cat = sequelize.define('cat', { 35 | name: Sequelize.STRING 36 | , spots: Sequelize.INTEGER 37 | }) 38 | sequelize.sync({force: true}) 39 | 40 | // install sequelizeStream 41 | import sequelizeStream from 'sequelize-stream' 42 | const stream = sequelizeStream(sequelize) 43 | 44 | // when the stream receives data, log 45 | stream.on('data', ({instance, event}) => console.log(event, instance.toJSON())) 46 | 47 | 48 | // examples 49 | Cat.bulkCreate([{name: 'fluffy'}, {name: 'spot'}]) 50 | // => 'create', {name: 'fluffy', id: 1} 51 | // => 'create', {name: 'spot', id: 2} 52 | 53 | Cat.create({name: 'sparky'}) 54 | // => 'create', {name: 'sparky', id: 3} 55 | .then((sparky) => { 56 | return sparky.update({spots: 2}) 57 | }) 58 | // => 'update', {name: 'sparky', spots: 2, id: 3} 59 | .tap((sparky) => { 60 | return Cat.update({spots: 1}, {where: {name: 'sparky'}}) 61 | }) 62 | // => 'update', {name: 'sparky', spots: 1, id: 3} 63 | .then((sparky) => { 64 | sparky.destroy() 65 | }) 66 | // => 'destroy', {name: 'sparky', spots: 1, id: 3} 67 | 68 | // NOTE: bulk destroy doesn't work due to Sequelize limitations. 69 | ``` 70 | 71 | ## Methods 72 | ### sequelizeStream `( sequelize)` => `` 73 | Pass a sequelize instance (`new Sequelize()`), and get back a standard node.js object readable stream. Subscribe to get events on all models as they go through your sequelize instance. 74 | 75 | ## Events 76 | ### data `( { event, instance})` 77 | The stream will emit objects with keys of `event` and `instance`. 78 | 79 | ```js 80 | const onData = ({event, instance} => { 81 | console.log(`${instance._modelOptions.name.singular} had a ${event} event`) 82 | // might log something like 'cat had a create event' 83 | }) 84 | ``` 85 | 86 | 87 | ## Caveats 88 | ### Bulk Destroy 89 | `Model.destroy({where})` doesn't work because there's no good way to get affected instances and be sure they were actually deleted. Regular destroy does work though (`instance.destroy()`). You should use `Model.destroy({where, individualHooks: true})` if you want stream events on the bulk method. 90 | 91 | ### Bulk Update 92 | `Model.update({where})` works, but `instance.previous()` and `instance.changed()` will note return anything because there's no good way to get affected instances from Sequelize. Instead, you receive new instances which are ignorant of changes. Regular update does work though (`instance.update()`). You should use `Model.update({where, individualHooks: true})` if you want stream events on the bulk method. 93 | 94 | ## Tests 95 | Tests are in [AVA](https://github.com/avajs/ava). 96 | 97 | * `npm test` will run the tests 98 | * `npm run tdd` will run the tests on every file change. 99 | 100 | ## Developing 101 | To publish, run `npm run release -- [{patch,minor,major}]` 102 | 103 | _NOTE: you might need to `sudo ln -s /usr/local/bin/node /usr/bin/node` to ensure node is in your path for the git hooks to work_ 104 | 105 | ### Requirements 106 | * **npm > 2.0.0** So that passing args to a npm script will work. `npm i -g npm` 107 | * **git > 1.8.3** So that `git push --follow-tags` will work. `brew install git` 108 | 109 | ## License 110 | 111 | Artistic 2.0 © [Joey Baker](https://byjoeybaker.com) and contributors. A copy of the license can be found in the file `LICENSE`. 112 | 113 | 114 | [npm-url]: https://www.npmjs.com/package/sequelize-stream 115 | [npm-image]: https://badge.fury.io/js/sequelize-stream.svg 116 | [circleci-url]: https://circleci.com/gh/joeybaker/sequelize-stream 117 | [circleci-image]: https://circleci.com/gh/joeybaker/sequelize-stream/tree/master.svg?style=svg 118 | [daviddm-url]: https://david-dm.org/joeybaker/sequelize-stream.svg?theme=shields.io 119 | [daviddm-image]: https://david-dm.org/joeybaker/sequelize-stream 120 | 121 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "lean-imports", 4 | "filenames", 5 | "ava" 6 | ], 7 | "extends": [ 8 | "plugin:ava/recommended" 9 | ], 10 | "parser": "babel-eslint", 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "sourceType": "module" 17 | }, 18 | "env": { 19 | "node": true, 20 | "es6": true 21 | }, 22 | "rules": { 23 | "comma-dangle": [ 24 | 2, 25 | "never" 26 | ], 27 | "no-cond-assign": 2, 28 | "no-constant-condition": 2, 29 | "no-control-regex": 2, 30 | "no-debugger": 2, 31 | "no-dupe-keys": 2, 32 | "no-empty": 2, 33 | "no-console": [ 34 | 1, 35 | { 36 | "allow": [ 37 | "warn", 38 | "error", 39 | "info" 40 | ] 41 | } 42 | ], 43 | "no-empty-character-class": 2, 44 | "no-ex-assign": 2, 45 | "no-extra-boolean-cast": 2, 46 | "no-extra-parens": 0, 47 | "no-extra-semi": 2, 48 | "no-func-assign": 2, 49 | "no-inner-declarations": 2, 50 | "no-invalid-regexp": 2, 51 | "no-irregular-whitespace": 2, 52 | "no-negated-in-lhs": 2, 53 | "no-obj-calls": 2, 54 | "no-regex-spaces": 2, 55 | "quote-props": [ 56 | 1, 57 | "as-needed" 58 | ], 59 | "no-sparse-arrays": 2, 60 | "no-unreachable": 2, 61 | "use-isnan": 2, 62 | "valid-typeof": 2, 63 | "block-scoped-var": 0, 64 | "consistent-return": 0, 65 | "curly": [ 66 | 1, 67 | "multi-line" 68 | ], 69 | "default-case": 2, 70 | "dot-notation": 2, 71 | "eqeqeq": [2, "smart"], 72 | "guard-for-in": 2, 73 | "no-alert": 1, 74 | "no-caller": 2, 75 | "no-div-regex": 2, 76 | "no-eq-null": 0, 77 | "no-eval": 2, 78 | "no-extend-native": 2, 79 | "no-extra-bind": 2, 80 | "no-fallthrough": 2, 81 | "no-floating-decimal": 2, 82 | "no-implied-eval": 2, 83 | "no-iterator": 2, 84 | "no-labels": 2, 85 | "no-lone-blocks": 2, 86 | "no-loop-func": 2, 87 | "no-multi-spaces": 1, 88 | "no-multi-str": 2, 89 | "no-native-reassign": 2, 90 | "no-new": 2, 91 | "no-new-func": 2, 92 | "no-new-wrappers": 2, 93 | "no-octal": 2, 94 | "no-octal-escape": 2, 95 | "no-proto": 2, 96 | "no-redeclare": 2, 97 | "no-return-assign": [ 98 | 2, 99 | "except-parens" 100 | ], 101 | "no-script-url": 2, 102 | "no-self-compare": 2, 103 | "no-sequences": 2, 104 | "no-throw-literal": 2, 105 | "no-unused-expressions": 0, 106 | "no-with": 2, 107 | "radix": 2, 108 | "vars-on-top": 2, 109 | "wrap-iife": 2, 110 | "yoda": [ 111 | 1, 112 | "never" 113 | ], 114 | "strict": [ 115 | 2, 116 | "global" 117 | ], 118 | "no-delete-var": 2, 119 | "no-label-var": 2, 120 | "no-shadow": 2, 121 | "no-shadow-restricted-names": 2, 122 | "no-undef": 2, 123 | "no-undef-init": 2, 124 | "no-undefined": 2, 125 | "no-unused-vars": [ 126 | 2, 127 | "all" 128 | ], 129 | "no-use-before-define": 2, 130 | "handle-callback-err": 2, 131 | "no-mixed-requires": 0, 132 | "no-new-require": 2, 133 | "no-path-concat": 2, 134 | "indent": [ 135 | 1, 136 | 2 137 | ], 138 | "brace-style": [ 139 | 1, 140 | "stroustrup", 141 | { 142 | "allowSingleLine": false 143 | } 144 | ], 145 | "comma-spacing": [ 146 | 1, 147 | { 148 | "before": false, 149 | "after": true 150 | } 151 | ], 152 | "comma-style": [ 153 | 2, 154 | "first" 155 | ], 156 | "consistent-this": [ 157 | 1, 158 | "self" 159 | ], 160 | "eol-last": 2, 161 | "func-names": 1, 162 | "func-style": [ 163 | 2, 164 | "expression" 165 | ], 166 | "key-spacing": [ 167 | 1, 168 | { 169 | "beforeColon": false, 170 | "afterColon": true 171 | } 172 | ], 173 | "max-nested-callbacks": [ 174 | 2, 175 | 3 176 | ], 177 | "new-cap": 2, 178 | "new-parens": 2, 179 | "no-array-constructor": 0, 180 | "no-inline-comments": 1, 181 | "no-lonely-if": 0, 182 | "no-mixed-spaces-and-tabs": 2, 183 | "no-multiple-empty-lines": 2, 184 | "no-nested-ternary": 0, 185 | "no-new-object": 2, 186 | "semi-spacing": [ 187 | 2, 188 | { 189 | "before": false, 190 | "after": true 191 | } 192 | ], 193 | "no-spaced-func": 1, 194 | "no-ternary": 0, 195 | "no-trailing-spaces": 2, 196 | "no-underscore-dangle": 0, 197 | "one-var": [ 198 | 1, 199 | { 200 | "var": "always", 201 | "let": "never", 202 | "const": "never" 203 | } 204 | ], 205 | "operator-assignment": [ 206 | 2, 207 | "always" 208 | ], 209 | "padded-blocks": [ 210 | 1, 211 | "never" 212 | ], 213 | "quotes": [ 214 | 2, 215 | "single", 216 | {"avoidEscape": true, "allowTemplateLiterals": true} 217 | ], 218 | "semi": [ 219 | 2, 220 | "never" 221 | ], 222 | "sort-vars": 0, 223 | "keyword-spacing": 1, 224 | "space-before-blocks": 0, 225 | "space-before-function-paren": [ 226 | 1, 227 | "always" 228 | ], 229 | "object-curly-spacing": [ 230 | 1, 231 | "never" 232 | ], 233 | "array-bracket-spacing": [ 234 | 1, 235 | "never" 236 | ], 237 | "space-in-parens": [ 238 | 1, 239 | "never" 240 | ], 241 | "space-infix-ops": 2, 242 | "space-unary-ops": [ 243 | 1, 244 | { 245 | "words": true, 246 | "nonwords": false 247 | } 248 | ], 249 | "spaced-comment": [ 250 | 1, 251 | "always", 252 | { 253 | "exceptions": [ 254 | "-" 255 | ] 256 | } 257 | ], 258 | "wrap-regex": 0, 259 | "constructor-super": 2, 260 | "no-this-before-super": 2, 261 | "require-yield": 2, 262 | "prefer-spread": 1, 263 | "no-useless-call": 1, 264 | "no-invalid-this": 2, 265 | "no-const-assign": 2, 266 | "no-class-assign": 2, 267 | "callback-return": [ 268 | 0, 269 | [ 270 | "callback", 271 | "cb", 272 | "done", 273 | "next" 274 | ] 275 | ], 276 | "no-var": 1, 277 | "prefer-const": 1, 278 | "no-magic-numbers": [ 279 | 1, 280 | { 281 | "ignore": [ 282 | -1, 283 | 0, 284 | 1, 285 | 2 286 | ], 287 | "detectObjects": true, 288 | "enforceConst": true 289 | } 290 | ], 291 | "arrow-spacing": [ 292 | 1, 293 | { 294 | "before": true, 295 | "after": true 296 | } 297 | ], 298 | "arrow-parens": 1, 299 | "no-confusing-arrow": 2, 300 | "jsx-quotes": [ 301 | 1, 302 | "prefer-double" 303 | ], 304 | "lean-imports/import": [ 305 | 1, 306 | [ 307 | "lodash" 308 | ] 309 | ], 310 | "filenames/match-regex": [ 311 | 2, 312 | "^[a-z-\\.]+$" 313 | ], 314 | "ava/prefer-async-await": 0 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Joey Baker and Contributors 2 | All rights reserved. 3 | 4 | This code is released under the Artistic License 2.0. 5 | The text of the License follows: 6 | 7 | 8 | -------- 9 | 10 | 11 | The Artistic License 2.0 12 | 13 | Copyright (c) 2000-2006, The Perl Foundation. 14 | 15 | Everyone is permitted to copy and distribute verbatim copies 16 | of this license document, but changing it is not allowed. 17 | 18 | Preamble 19 | 20 | This license establishes the terms under which a given free software 21 | Package may be copied, modified, distributed, and/or redistributed. 22 | The intent is that the Copyright Holder maintains some artistic 23 | control over the development of that Package while still keeping the 24 | Package available as open source and free software. 25 | 26 | You are always permitted to make arrangements wholly outside of this 27 | license directly with the Copyright Holder of a given Package. If the 28 | terms of this license do not permit the full use that you propose to 29 | make of the Package, you should contact the Copyright Holder and seek 30 | a different licensing arrangement. 31 | 32 | Definitions 33 | 34 | "Copyright Holder" means the individual(s) or organization(s) 35 | named in the copyright notice for the entire Package. 36 | 37 | "Contributor" means any party that has contributed code or other 38 | material to the Package, in accordance with the Copyright Holder's 39 | procedures. 40 | 41 | "You" and "your" means any person who would like to copy, 42 | distribute, or modify the Package. 43 | 44 | "Package" means the collection of files distributed by the 45 | Copyright Holder, and derivatives of that collection and/or of 46 | those files. A given Package may consist of either the Standard 47 | Version, or a Modified Version. 48 | 49 | "Distribute" means providing a copy of the Package or making it 50 | accessible to anyone else, or in the case of a company or 51 | organization, to others outside of your company or organization. 52 | 53 | "Distributor Fee" means any fee that you charge for Distributing 54 | this Package or providing support for this Package to another 55 | party. It does not mean licensing fees. 56 | 57 | "Standard Version" refers to the Package if it has not been 58 | modified, or has been modified only in ways explicitly requested 59 | by the Copyright Holder. 60 | 61 | "Modified Version" means the Package, if it has been changed, and 62 | such changes were not explicitly requested by the Copyright 63 | Holder. 64 | 65 | "Original License" means this Artistic License as Distributed with 66 | the Standard Version of the Package, in its current version or as 67 | it may be modified by The Perl Foundation in the future. 68 | 69 | "Source" form means the source code, documentation source, and 70 | configuration files for the Package. 71 | 72 | "Compiled" form means the compiled bytecode, object code, binary, 73 | or any other form resulting from mechanical transformation or 74 | translation of the Source form. 75 | 76 | 77 | Permission for Use and Modification Without Distribution 78 | 79 | (1) You are permitted to use the Standard Version and create and use 80 | Modified Versions for any purpose without restriction, provided that 81 | you do not Distribute the Modified Version. 82 | 83 | 84 | Permissions for Redistribution of the Standard Version 85 | 86 | (2) You may Distribute verbatim copies of the Source form of the 87 | Standard Version of this Package in any medium without restriction, 88 | either gratis or for a Distributor Fee, provided that you duplicate 89 | all of the original copyright notices and associated disclaimers. At 90 | your discretion, such verbatim copies may or may not include a 91 | Compiled form of the Package. 92 | 93 | (3) You may apply any bug fixes, portability changes, and other 94 | modifications made available from the Copyright Holder. The resulting 95 | Package will still be considered the Standard Version, and as such 96 | will be subject to the Original License. 97 | 98 | 99 | Distribution of Modified Versions of the Package as Source 100 | 101 | (4) You may Distribute your Modified Version as Source (either gratis 102 | or for a Distributor Fee, and with or without a Compiled form of the 103 | Modified Version) provided that you clearly document how it differs 104 | from the Standard Version, including, but not limited to, documenting 105 | any non-standard features, executables, or modules, and provided that 106 | you do at least ONE of the following: 107 | 108 | (a) make the Modified Version available to the Copyright Holder 109 | of the Standard Version, under the Original License, so that the 110 | Copyright Holder may include your modifications in the Standard 111 | Version. 112 | 113 | (b) ensure that installation of your Modified Version does not 114 | prevent the user installing or running the Standard Version. In 115 | addition, the Modified Version must bear a name that is different 116 | from the name of the Standard Version. 117 | 118 | (c) allow anyone who receives a copy of the Modified Version to 119 | make the Source form of the Modified Version available to others 120 | under 121 | 122 | (i) the Original License or 123 | 124 | (ii) a license that permits the licensee to freely copy, 125 | modify and redistribute the Modified Version using the same 126 | licensing terms that apply to the copy that the licensee 127 | received, and requires that the Source form of the Modified 128 | Version, and of any works derived from it, be made freely 129 | available in that license fees are prohibited but Distributor 130 | Fees are allowed. 131 | 132 | 133 | Distribution of Compiled Forms of the Standard Version 134 | or Modified Versions without the Source 135 | 136 | (5) You may Distribute Compiled forms of the Standard Version without 137 | the Source, provided that you include complete instructions on how to 138 | get the Source of the Standard Version. Such instructions must be 139 | valid at the time of your distribution. If these instructions, at any 140 | time while you are carrying out such distribution, become invalid, you 141 | must provide new instructions on demand or cease further distribution. 142 | If you provide valid instructions or cease distribution within thirty 143 | days after you become aware that the instructions are invalid, then 144 | you do not forfeit any of your rights under this license. 145 | 146 | (6) You may Distribute a Modified Version in Compiled form without 147 | the Source, provided that you comply with Section 4 with respect to 148 | the Source of the Modified Version. 149 | 150 | 151 | Aggregating or Linking the Package 152 | 153 | (7) You may aggregate the Package (either the Standard Version or 154 | Modified Version) with other packages and Distribute the resulting 155 | aggregation provided that you do not charge a licensing fee for the 156 | Package. Distributor Fees are permitted, and licensing fees for other 157 | components in the aggregation are permitted. The terms of this license 158 | apply to the use and Distribution of the Standard or Modified Versions 159 | as included in the aggregation. 160 | 161 | (8) You are permitted to link Modified and Standard Versions with 162 | other works, to embed the Package in a larger work of your own, or to 163 | build stand-alone binary or bytecode versions of applications that 164 | include the Package, and Distribute the result without restriction, 165 | provided the result does not expose a direct interface to the Package. 166 | 167 | 168 | Items That are Not Considered Part of a Modified Version 169 | 170 | (9) Works (including, but not limited to, modules and scripts) that 171 | merely extend or make use of the Package, do not, by themselves, cause 172 | the Package to be a Modified Version. In addition, such works are not 173 | considered parts of the Package itself, and are not subject to the 174 | terms of this license. 175 | 176 | 177 | General Provisions 178 | 179 | (10) Any use, modification, and distribution of the Standard or 180 | Modified Versions is governed by this Artistic License. By using, 181 | modifying or distributing the Package, you accept this license. Do not 182 | use, modify, or distribute the Package, if you do not accept this 183 | license. 184 | 185 | (11) If your Modified Version has been derived from a Modified 186 | Version made by someone other than you, you are nevertheless required 187 | to ensure that your Modified Version complies with the requirements of 188 | this license. 189 | 190 | (12) This license does not grant you the right to use any trademark, 191 | service mark, tradename, or logo of the Copyright Holder. 192 | 193 | (13) This license includes the non-exclusive, worldwide, 194 | free-of-charge patent license to make, have made, use, offer to sell, 195 | sell, import and otherwise transfer the Package with respect to any 196 | patent claims licensable by the Copyright Holder that are necessarily 197 | infringed by the Package. If you institute patent litigation 198 | (including a cross-claim or counterclaim) against any party alleging 199 | that the Package constitutes direct or contributory patent 200 | infringement, then this Artistic License to you shall terminate on the 201 | date that such litigation is filed. 202 | 203 | (14) Disclaimer of Warranty: 204 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 205 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 206 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 207 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 208 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 209 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 210 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 211 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 212 | --------------------------------------------------------------------------------