feat: dataSource.execute(cmd, args, opts, cb)

Implement a new helper API for calling connector's "execute" method
in a promise-friendly way.
This commit is contained in:
Miroslav Bajtoš 2018-12-06 13:58:47 +01:00
parent 3c2669ed7d
commit fda332d60b
No known key found for this signature in database
GPG Key ID: 6F2304BA9361C7E3
3 changed files with 151 additions and 0 deletions

View File

@ -2594,6 +2594,54 @@ DataSource.prototype.ping = function(cb) {
return cb.promise;
};
/**
* Execute an arbitrary command. The commands are connector specific,
* please refer to the documentation of your connector for more details.
*
* @param command String|Object The command to execute, e.g. an SQL query.
* @param [args] Array Parameters values to set in the command.
* @param [options] Object Additional options, e.g. the transaction to use.
* @returns Promise A promise of the result
*/
DataSource.prototype.execute = function(command, args = [], options = {}) {
assert(typeof command === 'string' || typeof command === 'object',
'"command" must be a string or an object.');
assert(typeof args === 'object',
'"args" must be an object, an array or undefined.');
assert(typeof options === 'object',
'"options" must be an object or undefined.');
if (!this.connector) {
return Promise.reject(errorNotImplemented(
`DataSource "${this.name}" is missing a connector to execute the command.`
));
}
if (!this.connector.execute) {
return Promise.reject(new errorNotImplemented(
`The connector "${this.connector.name}" used by dataSource "${this.name}" ` +
'does not implement "execute()" API.'
));
}
return new Promise((resolve, reject) => {
this.connector.execute(command, args, options, onExecuted);
function onExecuted(err, result) {
if (err) return reject(err);
if (arguments.length > 2) {
result = Array.prototype.slice.call(arguments, 1);
}
resolve(result);
}
});
function errorNotImplemented(msg) {
const err = new Error(msg);
err.code = 'NOT_IMPLEMENTED';
return err;
}
};
/*! The hidden property call is too expensive so it is not used that much
*/
/**

View File

@ -352,4 +352,100 @@ describe('DataSource', function() {
.should.not.containEql('TestModel');
});
});
describe('execute', () => {
let ds;
beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
it('calls connnector to execute the command', async () => {
let called = 'not called';
ds.connector.execute = function(command, args, options, callback) {
called = {command, args, options};
callback(null, 'a-result');
};
const result = await ds.execute(
'command',
['arg1', 'arg2'],
{'a-flag': 'a-value'}
);
result.should.be.equal('a-result');
called.should.be.eql({
command: 'command',
args: ['arg1', 'arg2'],
options: {'a-flag': 'a-value'},
});
});
it('supports shorthand version (cmd)', async () => {
let called = 'not called';
ds.connector.execute = function(command, args, options, callback) {
called = {command, args, options};
callback(null, 'a-result');
};
const result = await ds.execute('command');
result.should.be.equal('a-result');
called.should.be.eql({
command: 'command',
args: [],
options: {},
});
});
it('supports shorthand version (cmd, args)', async () => {
let called = 'not called';
ds.connector.execute = function(command, args, options, callback) {
called = {command, args, options};
callback(null, 'a-result');
};
await ds.execute('command', ['arg1', 'arg2']);
called.should.be.eql({
command: 'command',
args: ['arg1', 'arg2'],
options: {},
});
});
it('converts multiple callbacks arguments into a promise resolved with an array', async () => {
ds.connector.execute = function(command, args, options, callback) {
callback(null, 'result1', 'result2');
};
const result = await ds.execute('command');
result.should.eql(['result1', 'result2']);
});
it('allows args as object', async () => {
let called = 'not called';
ds.connector.execute = function(command, args, options, callback) {
called = {command, args, options};
callback();
};
// See https://www.npmjs.com/package/loopback-connector-neo4j-graph
const command = 'MATCH (u:User {email: {email}}) RETURN u';
await ds.execute(command, {email: 'alice@example.com'});
called.should.be.eql({
command,
args: {email: 'alice@example.com'},
options: {},
});
});
it('throws NOT_IMPLEMENTED when no connector is provided', () => {
ds.connector = undefined;
return ds.execute('command').should.be.rejectedWith({
code: 'NOT_IMPLEMENTED',
});
});
it('throws NOT_IMPLEMENTED for connectors not implementing execute', () => {
ds.connector.execute = undefined;
return ds.execute('command').should.be.rejectedWith({
code: 'NOT_IMPLEMENTED',
});
});
});
});

View File

@ -178,4 +178,11 @@ export declare class DataSource extends EventEmitter {
connect(callback?: Callback): PromiseOrVoid;
disconnect(callback?: Callback): PromiseOrVoid;
ping(callback?: Callback): PromiseOrVoid;
// Only promise variant, callback is intentionally not supported.
execute(
command: string | object,
args?: any[] | object,
options?: Options
): Promise<any>;
}