Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions src/Kuzzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const
SecurityController = require('./controllers/security'),
MemoryStorageController = require('./controllers/memoryStorage'),
BaseController = require('./controllers/base'),
uuidv4 = require('./uuidv4');
uuidv4 = require('./uuidv4'),
proxify = require('./proxify');

const
events = [
Expand Down Expand Up @@ -87,6 +88,14 @@ class Kuzzle extends KuzzleEventEmitter {
}
this.queuing = false;
this.replayInterval = 10;

this._jwt = undefined;

return proxify(this, {
seal: true,
name: 'kuzzle',
exposeApi: true
});
}

get autoQueue () {
Expand Down Expand Up @@ -460,20 +469,23 @@ Discarded request: ${JSON.stringify(request)}`));
throw new Error('You must provide a valid accessor.');
}

if (this[accessor]) {
if (this.__proxy__ ? this.__proxy__.hasProp(accessor) : this[accessor]) {
throw new Error(`There is already a controller with the accessor '${accessor}'. Please use another one.`);
}

const controller = new ControllerClass(this);

if (!(controller.name && controller.name.length > 0)) {
throw new Error('Controllers must have a name.');
}

if (controller.kuzzle !== this) {
throw new Error('You must pass the Kuzzle SDK instance to the parent constructor.');
}


if (this.__proxy__) {
this.__proxy__.registerProps(accessor);
}
this[accessor] = controller;

return this;
Expand Down
81 changes: 81 additions & 0 deletions src/proxify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@

const getOptions = options => ({
name: 'object',
seal: true,
sealGet: false,
deprecated: [],
exposeApi: false,
apiNamespace: '__proxy__',
...options
});

const getPropertyNames = (obj) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

(nitpicking) coding-style consistency

Suggested change
const getPropertyNames = (obj) => {
const getPropertyNames = obj => {

if (!obj) {
return [];
}
const methods = Object.getOwnPropertyNames(obj.prototype || obj);
const proto = Object.getPrototypeOf(obj);
return deleteDuplicates([
...methods,
...getPropertyNames(proto)
]);
};

const deleteDuplicates = arr => [...new Set(arr)];

const proxify = (obj, opts = {}) => {
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
throw Error('proxify only applies on non-null object');
}

const options = getOptions(opts);
const properties = ['inspect'];
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be a Set too


if (options.exposeApi) {
obj[options.apiNamespace] = {
registerProps: name => {
if (!properties.includes(name)) {
properties.push(name);
}
},
unregisterProps: name => {
const index = properties.indexOf(name);
if (index !== -1) {
properties.splice(index, 1);
}
},
hasProp: name => properties.includes(name)
};
}

properties.push.apply(properties, deleteDuplicates([
...Object.getOwnPropertyNames(obj),
...getPropertyNames(obj)
]));

const handler = {
get: (target, name) => {
if (options.sealGet && typeof name === 'string' && !properties.includes(name)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you should remove the typecheck made on name:

  • it can only be a string or a symbol anyway, since this is a getter trap
  • if I pass a non-existing symbol to your handler, I won't get any error even with sealGet active, which does not seem to be the intended behavior

throw new Error(`${options.name}.${name} is not defined`);
}
if (options.deprecated.includes(name)) {
console.warn(`Warning: ${options.name}.${name} is deprecated`);
Copy link
Contributor

Choose a reason for hiding this comment

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

you must add some kind of flag to trigger that warning only once per deprecated property accessed, otherwise this can clog the console up pretty quickly

}
return target[name];
},
set: (target, name, value) => {
if (options.seal && !properties.includes(name)) {
throw new Error(`setting a not defined '${name}' properties in '${options.name}' object`)
}
if (options.deprecated.includes(name)) {
console.warn(`Warning: ${options.name}.${name} is deprecated`);
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

}
target[name] = value;
return true;
}
};

return new Proxy(obj, handler);
};

module.exports = proxify;
125 changes: 125 additions & 0 deletions test/helpers/proxify.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const
should = require('should'),
sinon = require('sinon'),
proxify = require('../../src/proxify');

describe('proxify', () => {
let
warn,
srcObj;

beforeEach(() => {
warn = console.warn;
console.warn = sinon.stub();

srcObj = {
prop: 42,
method: () => {},
method2: () => {}
};
});

after(() => {
console.warn = warn;
});

it('should not throw if use object valid property', () => {
const obj = proxify(srcObj);
should.doesNotThrow(() => {
obj.prop += 1;
});
});

it('should not throw if use object unvalid property without seal', () => {
const obj = proxify(srcObj, { seal: false });
should.doesNotThrow(() => {
obj.prop2 = 42;
});
});

it('should throw if use object unvalid property', () => {
const obj = proxify(srcObj);
should.throws(() => {
obj.prop2 = 42;
});
});

it('should not warn if use non-deprecated property', () => {
const obj = proxify(srcObj, {
deprecated: ['method']
});
obj.method2();
should(console.warn).have.callCount(0);
});

it('should warn if use deprecated property', () => {
const obj = proxify(srcObj, {
deprecated: ['method']
});
obj.method();
should(console.warn).have.callCount(1);
});

it('should expose api to manipulate proxy props', () => {
const obj = proxify(srcObj, {
exposeApi: true,
});
should.doesNotThrow(() => {
should(obj.__proxy__).be.Object();
should(obj.__proxy__.registerProps).be.Function();
should(obj.__proxy__.unregisterProps).be.Function();
should(obj.__proxy__.hasProp).be.Function();
});
});

it('should expose api under custom namespace', () => {
const obj = proxify(srcObj, {
exposeApi: true,
apiNamespace: 'custom'
});
should.doesNotThrow(() => {
should(obj.custom).be.Object();
should(obj.custom.registerProps).be.Function();
should(obj.custom.unregisterProps).be.Function();
should(obj.custom.hasProp).be.Function();
});
});

it('should register new props', () => {
const obj = proxify(srcObj, {
exposeApi: true,
});
should.throws(() => {
obj.foo = 42;
});
obj.__proxy__.registerProps('foo');
should.doesNotThrow(() => {
obj.foo += 1;
});
});

it('should unregister props', () => {
const obj = proxify(srcObj, {
exposeApi: true,
});
should.doesNotThrow(() => {
obj.prop = 42;
});
obj.__proxy__.unregisterProps('prop');
should.throws(() => {
obj.prop += 1;
});
});

it('should check has props without warn', () => {
const obj = proxify(srcObj, {
exposeApi: true,
});
let res;
should.doesNotThrow(() => {
res = obj.__proxy__.hasProp('foo');
});
should(res).be.eql(false);
});

});
31 changes: 31 additions & 0 deletions test/kuzzle/proxy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const
should = require('should'),
sinon = require('sinon'),
Kuzzle = require('../../src/Kuzzle'),
AuthController = require('../../src/controllers/auth'),
BulkController = require('../../src/controllers/bulk'),
CollectionController = require('../../src/controllers/collection'),
DocumentController = require('../../src/controllers/document'),
IndexController = require('../../src/controllers/index'),
MemoryStorageController = require('../../src/controllers/memoryStorage'),
SecurityController = require('../../src/controllers/security'),
ServerController = require('../../src/controllers/server'),
RealTimeController = require('../../src/controllers/realtime'),
{
SocketIO,
WebSocket,
Http
} = require('../../src/protocols'),
ProtocolMock = require('../mocks/protocol.mock');

describe('Kuzzle proxy', () => {
const protocolMock = new ProtocolMock('somewhere');

it('should throw an error if one tries to set unvalid properties', () => {
const kuzzle = new Kuzzle(protocolMock);
should.throws(() => {
kuzzle.jvt = 'foobar';
});
});

});