This is going to be quite a story, strap in.
The Error
Start with a stack trace:
TypeError: First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.
at Bucket.query (/Users/erik/aurora/dev_watch/ow-back/node_modules/couchbase/lib/bucket.js:955:11)
at CbStoreAdapter._ensureGsiIndices (/Users/erik/aurora/dev_watch/ow-back/node_modules/ottoman/lib/cbstoreadapter.js:484:22)
at /Users/erik/aurora/dev_watch/ow-back/node_modules/ottoman/lib/cbstoreadapter.js:507:10
at handler (/Users/erik/aurora/dev_watch/ow-back/node_modules/ottoman/lib/cbstoreadapter.js:285:7)
at /Users/erik/aurora/dev_watch/ow-back/node_modules/couchbase/lib/bucketmgr.js:152:7
at IncomingMessage.<anonymous> (/Users/erik/aurora/dev_watch/ow-back/node_modules/couchbase/lib/bucketmgr.js:13:7)
at emitNone (events.js:85:20)
This is complaining that Ottoman is trying to run something that isn’t an N1qlQuery object.
This was a major blocker for us, so we dove deep, and found some bizarre stuff.
Research
First observation - nobody runs N1qlQueries, you actually run an N1qlStringQuery, which is a subtype of N1qlQuery defined by node_modules/couchbase/lib/n1qlquery.js.
That file itself is a little bit strange, because it defines this subtype AFTER doing module.exports (just an odd side note, probably not related). Using util.inherits, it states that an N1qlStringQuery is extended from N1qlQuery. So far, so good.
Inside of the couchbase node SDK in bucket.js, here’s where the error stems from:
if (query instanceof ViewQuery) {
return this._view(
'_view', query.ddoc, query.name, query.options, callback);
} else if (query instanceof SpatialQuery) {
return this._view(
'_spatial', query.ddoc, query.name, query.options, callback);
} else if (query instanceof N1qlQuery) {
return this._n1ql(
query, params, callback
);
} else if (query instanceof SearchQuery) {
return this._fts(
query, callback
);
} else {
throw new TypeError(
'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.');
}
So this part is clear – this error is cropping up because the N1qlStringQuery that is being passed by Ottoman’s index creation methods is failing the test. “query” is an instanceof N1qlStringQuery (also known as couchbase.N1qlQuery.Direct, odd naming there, but again just a side note.
The Big WTF Moment
So…reducing this to simplest terms, in some runtime environments:
var q = couchbase.N1qlQuery.fromString('select * from default');
console.log(q instanceof N1qlQuery);
Yup, that’s false, at least some times, in some places.
Wait…WTF? This doesn’t make sense for several reasons. Primarily because this works for most people, ottoman passes its build tests, but then also if you look at the definition, this error really doesn’t make any sense.
function N1qlStringQuery(str) {
this.options = {
statement: str
};
this.isAdhoc = true;
}
util.inherits(N1qlStringQuery, N1qlQuery);
N1qlQuery.Direct = N1qlStringQuery;
Further deepening the mystery, if you take this condition:
} else if (query instanceof N1qlQuery) {
return this._n1ql(
query, params, callback
);
And you hack it to be this:
} else if (query instanceof N1qlQuery || query instanceof N1qlQuery.Direct) {
return this._n1ql(
query, params, callback
);
It still doesn’t work.
Something deep and ugly is happening here.
Work-Around
Hi random internet person! I bet you got here by googling! Welcome welcome, unlike those other posts you found that didn’t help you, this will actually fix your issue!
But it’s an ugly hack. Here goes – that line in bucket.js, where it checks the type of the argument passed to the query function, we changed it to this:
else if (query instanceof N1qlQuery || typeof query.consistency === 'function')
This is an ugly form of duck typing, basically we’re claiming that if you have a consistency function then you “walk like an N1qlQuery” and you “talk like an N1qlQuery”. I think I’m totally going to programmer hell for that line of code, but whatever.
Further weirdness
Most people don’t seem to have this issue. Even on a dev team of many people, only one of us had it, and we can’t identify any config difference between those that do and don’t. Same codebase, same version of node, etc. etc.
Hints & Tips – things I’m not sure are related but might be:
The hack isn’t a solution, it just gets us past this. Deeper question is, why is this happening? Well – I’m not sure. But here are some observations that suggest it’s a deeper issue.
- Most of our app is written in es6, we don’t use prototypal inheritance ever, except when working with external libs like ottoman & couchbase.
- The use of util.inherits is actively discouraged in node. https://nodejs.org/docs/latest/api/util.html#util_util_inherits_constructor_superconstructor
- es6 style class inherits and util.inherts / use of node “typeof” are incompatible, other people who use util.inherits have reported similar problems in other contexts: https://github.com/nodejs/node/issues/4179
- Plenty of other people recommend using extends specifically to avoid breaking “instanceof” https://github.com/airbnb/javascript#constructors--extends