Skip to content

table.createLoadJob has mutually exclusive polymorphic return #640

@zamnuts

Description

@zamnuts

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:

  1. With callback and without await, Writable is returned
  2. With callback and with await, Writable is returned
  3. Without callback and without await, Promise (of Job? can't actually test this) is returned
  4. 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.

Metadata

Metadata

Assignees

Labels

api: bigqueryIssues related to the googleapis/nodejs-bigquery API.type: processA process-related concern. May include testing, release, or the like.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions