-
Notifications
You must be signed in to change notification settings - Fork 210
Description
Affects @google-cloud/[email protected]
The table.createLoadJob is a callback-first method that has been promisified via promisifyAll, i.e. it doesn't meet the requirements of a non-promise method (Stream/constructor/etc). However, when the source argument is a string, the current implementation attempts a synchronous return type of Stream.Writable (EventEmitter). See https://github.com/googleapis/nodejs-bigquery/blob/v4.7.0/src/table.ts#L1226-L1248
This polymorphic return is problematic:
- With callback and without await, Writable is returned
- With callback and with await, Writable is returned
- Without callback and without await, Promise (of Job? can't actually test this) is returned
- Without callback and with await, Promise of Job is returned
The corresponding overloads are available:
createLoadJob(source: string): Writable; // Never reached
createLoadJob(source: string, callback: JobCallback): Writable; // 1, 2
// 4 is not documented as an overload, but is possible in the concrete signature:
createLoadJob(source: string): Promise<JobResponse> {...}One of the tests expects a Writable without a supplied callback, but, upon promisifyAll refactor in the unit tests to actually be applied, it fails due to the method assuming to be used like a promise, which corresponds to scenario 3 in previous: https://github.com/googleapis/nodejs-bigquery/blob/v4.7.0/test/table.ts#L58-L64
In the current unit tests, this issue is not presented due to the promisifyAll stub, which removes the promisification functionality. See https://github.com/googleapis/nodejs-bigquery/blob/v4.7.0/test/table.ts#L58-L64
Example test cases (all succeed):
// fix the promisifyAll fake
const fakePfy = extend({}, pfy, {
promisifyAll: (c: Function) => {
if (c.name === 'Table') {
promisified = true;
}
pfy.promisifyAll(c); // ADDED THIS
},
});
// test cases
it('+cb +async', async () => {
sandbox.stub(table, 'createWriteStream_').returns(new stream.Writable());
const result = await table.createLoadJob(FILEPATH, () => {}); // noop
assert(result instanceof stream.Writable);
result.emit('job', {metadata: 'foo'});
});
it('+cb -async', (done) => {
sandbox.stub(table, 'createWriteStream_').returns(new stream.Writable());
const result = table.createLoadJob(FILEPATH, done);
assert(result instanceof stream.Writable);
result.emit('job', {metadata: 'bar'});
});
it('-cb +async', async () => {
const w = new stream.Writable();
sandbox.stub(table, 'createWriteStream_').returns(w);
setImmediate(() => w.emit('job', {metadata: 'qux'}));
const result = await table.createLoadJob(FILEPATH);
assert.deepStrictEqual(result, [{metadata: 'qux'}, 'qux']);
});
it('-cb -async', () => {
// cannot test this, table.createLoadJob always returns a Promise
});Typically, combining promises, callbacks, and streams in a single function signature is mutually exclusive.
If a Writable result is really desired that coincides with the createLoadJob(source: string): Writable; signature, I recommend implementing a createLoadJobStream(source: string): Writable; method which will not be promisified.