1 // mongo.js
  2 
  3 // NOTE 'Mongo' may be defined here or in MongoJS.cpp.  Add code to init, not to this constructor.
  4 if (typeof Mongo == "undefined") {
  5     Mongo = function(host) {
  6         this.init(host);
  7     };
  8 }
  9 
 10 if (!Mongo.prototype) {
 11     throw Error("Mongo.prototype not defined");
 12 }
 13 
 14 if (!Mongo.prototype.find)
 15     Mongo.prototype.find = function(ns, query, fields, limit, skip, batchSize, options) {
 16         throw Error("find not implemented");
 17     };
 18 if (!Mongo.prototype.insert)
 19     Mongo.prototype.insert = function(ns, obj) {
 20         throw Error("insert not implemented");
 21     };
 22 if (!Mongo.prototype.remove)
 23     Mongo.prototype.remove = function(ns, pattern) {
 24         throw Error("remove not implemented");
 25     };
 26 if (!Mongo.prototype.update)
 27     Mongo.prototype.update = function(ns, query, obj, upsert) {
 28         throw Error("update not implemented");
 29     };
 30 
 31 if (typeof mongoInject == "function") {
 32     mongoInject(Mongo.prototype);
 33 }
 34 
 35 Mongo.prototype.setSlaveOk = function(value) {
 36     if (value == undefined)
 37         value = true;
 38     this.slaveOk = value;
 39 };
 40 
 41 Mongo.prototype.getSlaveOk = function() {
 42     return this.slaveOk || false;
 43 };
 44 
 45 Mongo.prototype.getDB = function(name) {
 46     if ((jsTest.options().keyFile) &&
 47         ((typeof this.authenticated == 'undefined') || !this.authenticated)) {
 48         jsTest.authenticate(this);
 49     }
 50     // There is a weird issue where typeof(db._name) !== "string" when the db name
 51     // is created from objects returned from native C++ methods.
 52     // This hack ensures that the db._name is always a string.
 53     if (typeof(name) === "object") {
 54         name = name.toString();
 55     }
 56     return new DB(this, name);
 57 };
 58 
 59 Mongo.prototype.getDBs = function() {
 60     var res = this.getDB("admin").runCommand({"listDatabases": 1});
 61     if (!res.ok)
 62         throw _getErrorWithCode(res, "listDatabases failed:" + tojson(res));
 63     return res;
 64 };
 65 
 66 Mongo.prototype.adminCommand = function(cmd) {
 67     return this.getDB("admin").runCommand(cmd);
 68 };
 69 
 70 /**
 71  * Returns all log components and current verbosity values
 72  */
 73 Mongo.prototype.getLogComponents = function() {
 74     var res = this.adminCommand({getParameter: 1, logComponentVerbosity: 1});
 75     if (!res.ok)
 76         throw _getErrorWithCode(res, "getLogComponents failed:" + tojson(res));
 77     return res.logComponentVerbosity;
 78 };
 79 
 80 /**
 81  * Accepts optional second argument "component",
 82  * string of form "storage.journaling"
 83  */
 84 Mongo.prototype.setLogLevel = function(logLevel, component) {
 85     componentNames = [];
 86     if (typeof component === "string") {
 87         componentNames = component.split(".");
 88     } else if (component !== undefined) {
 89         throw Error("setLogLevel component must be a string:" + tojson(component));
 90     }
 91     var vDoc = {verbosity: logLevel};
 92 
 93     // nest vDoc
 94     for (var key, obj; componentNames.length > 0;) {
 95         obj = {};
 96         key = componentNames.pop();
 97         obj[key] = vDoc;
 98         vDoc = obj;
 99     }
100     var res = this.adminCommand({setParameter: 1, logComponentVerbosity: vDoc});
101     if (!res.ok)
102         throw _getErrorWithCode(res, "setLogLevel failed:" + tojson(res));
103     return res;
104 };
105 
106 Mongo.prototype.getDBNames = function() {
107     return this.getDBs().databases.map(function(z) {
108         return z.name;
109     });
110 };
111 
112 Mongo.prototype.getCollection = function(ns) {
113     var idx = ns.indexOf(".");
114     if (idx < 0)
115         throw Error("need . in ns");
116     var db = ns.substring(0, idx);
117     var c = ns.substring(idx + 1);
118     return this.getDB(db).getCollection(c);
119 };
120 
121 Mongo.prototype.toString = function() {
122     return "connection to " + this.host;
123 };
124 Mongo.prototype.tojson = Mongo.prototype.toString;
125 
126 /**
127  * Sets the read preference.
128  *
129  * @param mode {string} read preference mode to use. Pass null to disable read
130  *     preference.
131  * @param tagSet {Array.<Object>} optional. The list of tags to use, order matters.
132  *     Note that this object only keeps a shallow copy of this array.
133  */
134 Mongo.prototype.setReadPref = function(mode, tagSet) {
135     if ((this._readPrefMode === "primary") && (typeof(tagSet) !== "undefined") &&
136         (Object.keys(tagSet).length > 0)) {
137         // we allow empty arrays/objects or no tagSet for compatibility reasons
138         throw Error("Can not supply tagSet with readPref mode primary");
139     }
140     this._setReadPrefUnsafe(mode, tagSet);
141 };
142 
143 // Set readPref without validating. Exposed so we can test the server's readPref validation.
144 Mongo.prototype._setReadPrefUnsafe = function(mode, tagSet) {
145     this._readPrefMode = mode;
146     this._readPrefTagSet = tagSet;
147 };
148 
149 Mongo.prototype.getReadPrefMode = function() {
150     return this._readPrefMode;
151 };
152 
153 Mongo.prototype.getReadPrefTagSet = function() {
154     return this._readPrefTagSet;
155 };
156 
157 // Returns a readPreference object of the type expected by mongos.
158 Mongo.prototype.getReadPref = function() {
159     var obj = {}, mode, tagSet;
160     if (typeof(mode = this.getReadPrefMode()) === "string") {
161         obj.mode = mode;
162     } else {
163         return null;
164     }
165     // Server Selection Spec: - if readPref mode is "primary" then the tags field MUST
166     // be absent. Ensured by setReadPref.
167     if (Array.isArray(tagSet = this.getReadPrefTagSet())) {
168         obj.tags = tagSet;
169     }
170 
171     return obj;
172 };
173 
174 connect = function(url, user, pass) {
175     if (user && !pass)
176         throw Error("you specified a user and not a password.  " +
177                     "either you need a password, or you're using the old connect api");
178 
179     // Validate connection string "url" as "hostName:portNumber/databaseName"
180     //                                  or "hostName/databaseName"
181     //                                  or "databaseName"
182     // hostName may be an IPv6 address (with colons), in which case ":portNumber" is required
183     //
184     var urlType = typeof url;
185     if (urlType == "undefined") {
186         throw Error("Missing connection string");
187     }
188     if (urlType != "string") {
189         throw Error("Incorrect type \"" + urlType + "\" for connection string \"" + tojson(url) +
190                     "\"");
191     }
192     url = url.trim();
193     if (0 == url.length) {
194         throw Error("Empty connection string");
195     }
196     if (!url.startsWith("mongodb://")) {
197         var colon = url.lastIndexOf(":");
198         var slash = url.lastIndexOf("/");
199         if (0 == colon || 0 == slash) {
200             throw Error("Missing host name in connection string \"" + url + "\"");
201         }
202         if (colon == slash - 1 || colon == url.length - 1) {
203             throw Error("Missing port number in connection string \"" + url + "\"");
204         }
205         if (colon != -1 && colon < slash) {
206             var portNumber = url.substring(colon + 1, slash);
207             if (portNumber.length > 5 || !/^\d*$/.test(portNumber) ||
208                 parseInt(portNumber) > 65535) {
209                 throw Error("Invalid port number \"" + portNumber + "\" in connection string \"" +
210                             url + "\"");
211             }
212         }
213         if (slash == url.length - 1) {
214             throw Error("Missing database name in connection string \"" + url + "\"");
215         }
216     }
217 
218     var db;
219     if (url.startsWith("mongodb://")) {
220         chatty("connecting to: " + url);
221         db = new Mongo(url);
222         if (db.defaultDB.length == 0) {
223             db.defaultDB = "test";
224         }
225         db = db.getDB(db.defaultDB);
226     } else if (slash == -1) {
227         chatty("connecting to: 127.0.0.1:27017/" + url);
228         db = new Mongo().getDB(url);
229     } else {
230         var hostPart = url.substring(0, slash);
231         var dbPart = url.substring(slash + 1);
232         chatty("connecting to: " + hostPart + "/" + dbPart);
233         db = new Mongo(hostPart).getDB(dbPart);
234     }
235 
236     if (user && pass) {
237         if (!db.auth(user, pass)) {
238             throw Error("couldn't login");
239         }
240     }
241 
242     // Check server version
243     var serverVersion = db.version();
244     chatty("MongoDB server version: " + serverVersion);
245 
246     var shellVersion = version();
247     if (serverVersion.slice(0, 3) != shellVersion.slice(0, 3)) {
248         chatty("WARNING: shell and server versions do not match");
249     }
250 
251     return db;
252 };
253 
254 /** deprecated, use writeMode below
255  *
256  */
257 Mongo.prototype.useWriteCommands = function() {
258     return (this.writeMode() != "legacy");
259 };
260 
261 Mongo.prototype.forceWriteMode = function(mode) {
262     this._writeMode = mode;
263 };
264 
265 Mongo.prototype.hasWriteCommands = function() {
266     var hasWriteCommands = (this.getMinWireVersion() <= 2 && 2 <= this.getMaxWireVersion());
267     return hasWriteCommands;
268 };
269 
270 Mongo.prototype.hasExplainCommand = function() {
271     var hasExplain = (this.getMinWireVersion() <= 3 && 3 <= this.getMaxWireVersion());
272     return hasExplain;
273 };
274 
275 /**
276  * {String} Returns the current mode set. Will be commands/legacy/compatibility
277  *
278  * Sends isMaster to determine if the connection is capable of using bulk write operations, and
279  * caches the result.
280  */
281 
282 Mongo.prototype.writeMode = function() {
283 
284     if ('_writeMode' in this) {
285         return this._writeMode;
286     }
287 
288     // get default from shell params
289     if (_writeMode)
290         this._writeMode = _writeMode();
291 
292     // can't use "commands" mode unless server version is good.
293     if (this.hasWriteCommands()) {
294         // good with whatever is already set
295     } else if (this._writeMode == "commands") {
296         print("Cannot use commands write mode, degrading to compatibility mode");
297         this._writeMode = "compatibility";
298     }
299 
300     return this._writeMode;
301 };
302 
303 /**
304  * Returns true if the shell is configured to use find/getMore commands rather than the C++ client.
305  *
306  * Currently, the C++ client will always use OP_QUERY find and OP_GET_MORE.
307  */
308 Mongo.prototype.useReadCommands = function() {
309     return (this.readMode() === "commands");
310 };
311 
312 /**
313  * For testing, forces the shell to use the readMode specified in 'mode'. Must be either "commands"
314  * (use the find/getMore commands), "legacy" (use legacy OP_QUERY/OP_GET_MORE wire protocol reads),
315  * or "compatibility" (auto-detect mode based on wire version).
316  */
317 Mongo.prototype.forceReadMode = function(mode) {
318     if (mode !== "commands" && mode !== "compatibility" && mode !== "legacy") {
319         throw new Error("Mode must be one of {commands, compatibility, legacy}, but got: " + mode);
320     }
321 
322     this._readMode = mode;
323 };
324 
325 /**
326  * Get the readMode string (either "commands" for find/getMore commands, "legacy" for OP_QUERY find
327  * and OP_GET_MORE, or "compatibility" for detecting based on wire version).
328  */
329 Mongo.prototype.readMode = function() {
330     // Get the readMode from the shell params if we don't have one yet.
331     if (typeof _readMode === "function" && !this.hasOwnProperty("_readMode")) {
332         this._readMode = _readMode();
333     }
334 
335     if (this.hasOwnProperty("_readMode") && this._readMode !== "compatibility") {
336         // We already have determined our read mode. Just return it.
337         return this._readMode;
338     } else {
339         // We're in compatibility mode. Determine whether the server supports the find/getMore
340         // commands. If it does, use commands mode. If not, degrade to legacy mode.
341         try {
342             var hasReadCommands = (this.getMinWireVersion() <= 4 && 4 <= this.getMaxWireVersion());
343             // TODO SERVER-23219: DBCommandCursor doesn't route getMore and killCursors operations
344             // to the server that the cursor was originally established on. As a workaround, we make
345             // replica set connections use 'legacy' read mode because the underlying DBClientCursor
346             // will correctly route operations to the original server.
347             if (hasReadCommands && !this.isReplicaSetConnection()) {
348                 this._readMode = "commands";
349             } else {
350                 print("Cannot use 'commands' readMode, degrading to 'legacy' mode");
351                 this._readMode = "legacy";
352             }
353         } catch (e) {
354             // We failed trying to determine whether the remote node supports the find/getMore
355             // commands. In this case, we keep _readMode as "compatibility" and the shell should
356             // issue legacy reads. Next time around we will issue another isMaster to try to
357             // determine the readMode decisively.
358         }
359     }
360 
361     return this._readMode;
362 };
363 
364 //
365 // Write Concern can be set at the connection level, and is used for all write operations unless
366 // overridden at the collection level.
367 //
368 
369 Mongo.prototype.setWriteConcern = function(wc) {
370     if (wc instanceof WriteConcern) {
371         this._writeConcern = wc;
372     } else {
373         this._writeConcern = new WriteConcern(wc);
374     }
375 };
376 
377 Mongo.prototype.getWriteConcern = function() {
378     return this._writeConcern;
379 };
380 
381 Mongo.prototype.unsetWriteConcern = function() {
382     delete this._writeConcern;
383 };
384