Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix event listener duplication #2982

Merged
merged 6 commits into from Jan 31, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions bin/cli.js
Expand Up @@ -44,7 +44,10 @@ function initKnex(env, opts) {
checkLocalModule(env);
if (process.cwd() !== env.cwd) {
process.chdir(env.cwd);
console.log('Working directory changed to', color.magenta(tildify(env.cwd)));
console.log(
'Working directory changed to',
color.magenta(tildify(env.cwd))
);
}

if (!opts.knexfile) {
Expand Down Expand Up @@ -102,7 +105,10 @@ function invoke(env) {
.version(
color.blue('Knex CLI version: ', color.green(cliPkg.version)) +
'\n' +
color.blue('Local Knex version: ', color.green(env.modulePackage.version)) +
color.blue(
'Local Knex version: ',
color.green(env.modulePackage.version)
) +
'\n'
)
.option('--debug', 'Run with debugging.')
Expand Down
43 changes: 33 additions & 10 deletions src/util/make-knex.js
Expand Up @@ -4,7 +4,7 @@ import Migrator from '../migrate/Migrator';
import Seeder from '../seed/Seeder';
import FunctionHelper from '../functionhelper';
import QueryInterface from '../query/methods';
import { assign } from 'lodash';
import { assign, merge } from 'lodash';
import batchInsert from './batchInsert';
import * as bluebird from 'bluebird';

Expand All @@ -13,6 +13,7 @@ export default function makeKnex(client) {
function knex(tableName, options) {
return createQueryBuilder(knex.context, tableName, options);
}

redefineProperties(knex, client);
return knex;
}
Expand Down Expand Up @@ -81,15 +82,16 @@ function initContext(knexFn) {
withUserParams(params) {
const knexClone = shallowCloneFunction(knexFn); // We need to include getters in our clone
if (this.client) {
knexClone.client = Object.assign({}, this.client); // Clone client to avoid leaking listeners that are set on it
knexClone.client = Object.create(this.client.constructor.prototype); // Clone client to avoid leaking listeners that are set on it
merge(knexClone.client, this.client);
knexClone.client.config = Object.assign({}, this.client.config); // Clone client config to make sure they can be modified independently
const parentPrototype = Object.getPrototypeOf(this.client);
if (parentPrototype) {
Object.setPrototypeOf(knexClone.client, parentPrototype);
}
}

redefineProperties(knexClone, knexClone.client);
_copyEventListeners('query', knexFn, knexClone);
_copyEventListeners('query-error', knexFn, knexClone);
_copyEventListeners('query-response', knexFn, knexClone);
_copyEventListeners('start', knexFn, knexClone);
knexClone.userParams = params;
return knexClone;
},
Expand All @@ -99,6 +101,12 @@ function initContext(knexFn) {
knexFn.context = knexContext;
}
}
function _copyEventListeners(eventName, sourceKnex, targetKnex) {
const listeners = sourceKnex.listeners(eventName);
listeners.forEach((listener) => {
targetKnex.on(eventName, listener);
});
}

function redefineProperties(knex, client) {
// Allow chaining methods from the root object, before
Expand Down Expand Up @@ -194,21 +202,36 @@ function redefineProperties(knex, client) {
knex[key] = ee[key];
}

if (knex._internalListeners) {
knex._internalListeners.forEach(({ eventName, listener }) => {
knex.client.removeListener(eventName, listener); // Remove duplicates for copies
});
}
knex._internalListeners = [];

// Passthrough all "start" and "query" events to the knex object.
knex.client.on('start', function(obj) {
_addInternalListener(knex, 'start', (obj) => {
knex.emit('start', obj);
});
knex.client.on('query', function(obj) {
_addInternalListener(knex, 'query', (obj) => {
knex.emit('query', obj);
});
knex.client.on('query-error', function(err, obj) {
_addInternalListener(knex, 'query-error', (err, obj) => {
knex.emit('query-error', err, obj);
});
knex.client.on('query-response', function(response, obj, builder) {
_addInternalListener(knex, 'query-response', (response, obj, builder) => {
knex.emit('query-response', response, obj, builder);
});
}

function _addInternalListener(knex, eventName, listener) {
knex.client.on(eventName, listener);
knex._internalListeners.push({
eventName,
listener,
});
}

function createQueryBuilder(knexContext, tableName, options) {
const qb = knexContext.queryBuilder();
if (!tableName)
Expand Down
2 changes: 2 additions & 0 deletions test/index.js
Expand Up @@ -31,6 +31,8 @@ describe('Query Building Tests', function() {
require('./unit/schema/oracledb');
require('./unit/migrate/migration-list-resolver');
require('./unit/seed/seeder');
// require('./unit/interface'); ToDo Uncomment after fixed
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one looks like it got broken quite a while ago, I'll open a separate issue for it

require('./unit/knex');
});

describe('Integration Tests', function() {
Expand Down
4 changes: 2 additions & 2 deletions test/integration/builder/additional.js
Expand Up @@ -986,7 +986,7 @@ module.exports = function(knex) {
});
});

it('Event: does not duplicate listeners on a copy with user params', function() {
it('Event: preserves listeners on a copy with user params', function() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably users are going to be confused if listeners set on original knex suddenly disappear when we copy it with user params, so this is an improvement.

let queryCount = 0;

const onQueryResponse = function(response, obj, builder) {
Expand All @@ -1012,7 +1012,7 @@ module.exports = function(knex) {
})
.then(function() {
expect(Object.keys(knex._events).length).to.equal(1);
expect(Object.keys(knexCopy._events).length).to.equal(0);
expect(Object.keys(knexCopy._events).length).to.equal(1);
knex.removeListener('query-response', onQueryResponse);
expect(Object.keys(knex._events).length).to.equal(0);
expect(queryCount).to.equal(4);
Expand Down
15 changes: 15 additions & 0 deletions test/integration/helpers/knex-builder.js
@@ -0,0 +1,15 @@
const knex = require('../../../knex');
const config = require('../../knexfile');

/*
Please do not remove this file even though it is not referenced anywhere.
This helper is meant for local debugging of specific tests.
*/

function getSqliteKnex() {
return knex(config.sqlite3);
}

module.exports = {
getSqliteKnex,
};
64 changes: 62 additions & 2 deletions test/unit/knex.js
Expand Up @@ -98,10 +98,68 @@ describe('knex', () => {
const knexWithParams = knex.withUserParams({ userParam: '451' });

expect(knexWithParams.migrate.knex.userParams).to.deep.equal({
isProcessingDisabled: true,
postProcessResponse: undefined,
userParam: '451',
wrapIdentifier: undefined,
});
});

it('copying does not result in duplicate listeners', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of duplicate with test "adding listener to copy does not affect base knex"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are different, actually. One tests for what happens after you copy, another one what happens after you mutate the copy.

const knex = Knex({
client: 'sqlite',
});
const knexWithParams = knex.withUserParams();

expect(knex.client.listeners('start').length).to.equal(1);
expect(knex.client.listeners('query').length).to.equal(1);
expect(knex.client.listeners('query-error').length).to.equal(1);
expect(knex.client.listeners('query-response').length).to.equal(1);

expect(knexWithParams.client.listeners('start').length).to.equal(1);
expect(knexWithParams.client.listeners('query').length).to.equal(1);
expect(knexWithParams.client.listeners('query-error').length).to.equal(1);
expect(knexWithParams.client.listeners('query-response').length).to.equal(
1
);
});

it('listeners added to knex directly get copied correctly', () => {
const knex = Knex({
client: 'sqlite',
});
const onQueryResponse = function(response, obj, builder) {};
expect(knex.listeners('query-response').length).to.equal(0);
knex.on('query-response', onQueryResponse);

const knexWithParams = knex.withUserParams();

expect(knex.listeners('query-response').length).to.equal(1);
expect(knexWithParams.listeners('query-response').length).to.equal(1);
});

it('adding listener to copy does not affect base knex', () => {
const knex = Knex({
client: 'sqlite',
});

expect(knex.client.listeners('start').length).to.equal(1);
expect(knex.client.listeners('query').length).to.equal(1);
expect(knex.client.listeners('query-error').length).to.equal(1);
expect(knex.client.listeners('query-response').length).to.equal(1);

const knexWithParams = knex.withUserParams();
knexWithParams.client.on('query', (obj) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm starting to have second thoughts if this was a good idea to allow creating modifiable clones of knex instance which are sharing the same pool (feature came kind of accidentally). I think it is a really nice feature to have, but API could have separated cloning and setting same pools for them and setting user parameters of each instance more clearly... anyways test looks good 👍 Wanted to just mention how this small feature of user parameters has exploded a bit for allowing to set custom event handlers too, which is good.

knexWithParams.emit('query', obj);
});

expect(knex.client.listeners('start').length).to.equal(1);
expect(knex.client.listeners('query').length).to.equal(1);
expect(knex.client.listeners('query-error').length).to.equal(1);
expect(knex.client.listeners('query-response').length).to.equal(1);
expect(knexWithParams.client.listeners('query').length).to.equal(2);
});

it('sets correct postProcessResponse for builders instantiated from clone', () => {
const knex = Knex({
client: 'sqlite',
Expand Down Expand Up @@ -171,8 +229,10 @@ describe('knex', () => {
});

it('throws if client module has not been installed', () => {
expect(Knex({ client: 'oracle' })).to.throw(
/Knex: run\n$ npm install oracle/
expect(() => {
Knex({ client: 'oracledb', connection: {} });
}).to.throw(
"Knex: run\n$ npm install oracledb --save\nCannot find module 'oracledb'"
);
});

Expand Down