diff --git a/LICENSE.md b/LICENSE.md index 67bfabf..48e3efd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ Bedrock Non-Commercial License v1.0 =================================== -Copyright (c) 2011-2017 Digital Bazaar, Inc. +Copyright (c) 2011-2024 Digital Bazaar, Inc. All rights reserved. Summary diff --git a/README.md b/README.md index 473d800..1bbb3b8 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,10 @@ bedrock.start(); For documentation on database configuration, see [config.js](./lib/config.js). ### Connecting and Authenticating -MongoDB's documentation offers tons of great examples on how to authenticate -using a myriad number of connection strings. +MongoDB's documentation provides examples on how to authenticate using a myriad +number of connection strings. -[Mongo Node 3.5 Driver connect docs](http://mongodb.github.io/node-mongodb-native/3.5/tutorials/connect/) - -[Mongo Node 3.5 Driver atlas docs](https://docs.mongodb.com/drivers/node#connect-to-mongodb-atlas) +[Mongo Node Driver connect docs](https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/connect/) You can also connect to access-enabled mongo servers using some small changes to the `config.mongodb.connectOptions`: @@ -94,9 +92,9 @@ config.mongodb.url = 'mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example. ## Requirements * Linux or Mac OS X (also works on Windows with some coaxing) -* node.js >= 14.x -* npm >= 6.x -* mongodb ~= 4.x +* node.js >= 18.x +* npm >= 9.x +* mongodb >= 5.x * libkrb5-dev >= 1.x.x ## Setup @@ -105,7 +103,7 @@ config.mongodb.url = 'mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example. at [mongodb.org](http://docs.mongodb.org/manual/tutorial/add-user-administrator/) for your version of MongoDB. Version 4.2.x is currently supported. 2. [optional] Tweak your project's configuration settings; see - [Configuration](#configuration) or [Quick Examples](#quickexamples). + [Configuration](#configuration) or [Quick Examples](#quick-examples). ## API @@ -122,13 +120,12 @@ an error occurs, the returned promise rejects. If no error occurs, then once the promise resolves, the `collections` object will have keys that match the collection names and values that are instances of [mongodb-native][] -[Collection](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html). +[Collection](https://mongodb.github.io/node-mongodb-native/6.3/classes/Collection.html). ### createGridFSBucket(options) Creates and returns a new `GridFSBucket` from the native driver. Options are -the same as for `GridFSBucket`. The current client is used and the -`writeConcern` option defaults to the `writeOptions` config value. +the same as for `GridFSBucket`. The current client is used. ## Test Mode ### Drop Collections on Initialization @@ -152,4 +149,4 @@ bedrock.config.mongodb.dropCollections.collections = []; ``` [bedrock]: https://github.com/digitalbazaar/bedrock -[mongodb-native]: http://mongodb.github.io/node-mongodb-native/3.5/ +[mongodb-native]: https://www.mongodb.com/docs/drivers/node/current/ diff --git a/lib/authn.js b/lib/authn.js index 56a18cf..334401d 100644 --- a/lib/authn.js +++ b/lib/authn.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as urls from './urls.js'; @@ -14,19 +14,19 @@ const {util: {BedrockError}} = bedrock; export async function openDatabase(options) { const config = bedrock.config.mongodb; - // copy the config stuff related to connecting + const opts = { - database: config.name, - authentication: {...config.authentication}, - // authSource should be set in connectOptions connectOptions: {...config.connectOptions}, writeOptions: {...config.writeOptions}, ...options }; - // if a `url` was not specified, create one from the `config` - if(!opts.url) { + if(config.url) { + opts.url = config.url; + } else { opts.url = urls.create(config); + opts.database = config.name; + opts.authentication = {...config.authentication}; } // do unauthenticated connection to mongo server to check @@ -35,13 +35,6 @@ export async function openDatabase(options) { const serverInfo = await admin.serverInfo(null); _checkServerVersion({serverInfo, config}); - // check if server supports roles; if not, can't authenticate - if(!_usesRoles(serverInfo)) { - const stringVersion = serverInfo.versionArray.join('.'); - throw new BedrockError( - `MongoDB server version "${stringVersion}" is unsupported.`, - 'NotSupportedError'); - } // makes an unauthenticated call to the server // to see if auth is required const authRequired = await _isAuthnRequired({config, admin}); @@ -49,7 +42,7 @@ export async function openDatabase(options) { // if authRequired create an auth object for Mongo; // otherwise `auth` will be passed as `null` and success will rely on other // config options such as the url for the server - if(authRequired) { + if(!opts.url && authRequired) { _addAuthOptions({options: opts, config}); } @@ -69,8 +62,8 @@ async function _connect(options) { if(!options.init) { logger.info('connecting to database: ' + urls.sanitize(options.url)); } - const {writeConcern} = options.writeOptions; - let connectOptions = {...options.connectOptions, writeConcern}; + + let connectOptions = {...options.connectOptions}; // socket related options used to be an object // they are now just general options in connectOptions if('socketOptions' in options.connectOptions) { @@ -80,20 +73,14 @@ async function _connect(options) { }; delete connectOptions.socketOptions; } + const client = await MongoClient.connect(options.url, connectOptions); + const db = client.db(); const ping = await db.admin().ping(); - logger.debug( - 'database connection succeeded: db=' + db.databaseName + - ' username=' + connectOptions?.auth?.user, {ping}); - return {client, db}; -} + logger.debug('database connection succeeded: db=' + db.databaseName, {ping}); -function _usesRoles(serverInfo) { - // >= Mongo 2.6 uses user roles - return ( - (serverInfo.versionArray[0] == 2 && serverInfo.versionArray[1] >= 6) || - (serverInfo.versionArray[0] > 2)); + return {client, db}; } /** @@ -128,7 +115,7 @@ async function _isAuthnRequired({config, admin}) { function _addAuthOptions({options, config}) { options.connectOptions.auth = { - user: config.username, + username: config.username, password: config.password }; // authSource is the database to authenticate against diff --git a/lib/config.js b/lib/config.js index 80fb46b..216fd07 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2012 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ import {config} from '@bedrock/core'; @@ -39,12 +39,8 @@ config.mongodb.authentication = { }; // this is used when making connections to the database config.mongodb.connectOptions = { - useUnifiedTopology: true, - serverSelectionTimeoutMS: 30000, - autoReconnect: false, - useNewUrlParser: true, - // promotes binary BSON values to native Node.js buffers promoteBuffers: true, + serverSelectionTimeoutMS: 30000, // it is recommended to set either ssl or tls to true in production // ssl: true // tls: true @@ -55,16 +51,12 @@ config.mongodb.connectOptions = { // this is used when writing to the database config.mongodb.writeOptions = { - writeConcern: { - w: 'majority', - j: true, - }, forceServerObjectId: true, }; config.mongodb.requirements = {}; // server version requirement with server-style string -config.mongodb.requirements.serverVersion = '>=4.2'; +config.mongodb.requirements.serverVersion = '>=5'; // this is used by _createUser to add a user as an admin to a collection // config.mongodb.collection = 'admin-collection'; diff --git a/lib/exceptions.js b/lib/exceptions.js index 1907f9f..8c35b21 100644 --- a/lib/exceptions.js +++ b/lib/exceptions.js @@ -1,15 +1,17 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ export const MDBE_ERROR = 'MongoError'; export const WRITE_ERROR = 'WriteError'; export const BULK_WRITE_ERROR = 'BulkWriteError'; export const WRITE_CONCERN_ERROR = 'WriteConcernError'; +export const MONGO_SERVER_ERROR = 'MongoServerError'; export const MDBE_ERRORS = [ MDBE_ERROR, WRITE_ERROR, BULK_WRITE_ERROR, - WRITE_CONCERN_ERROR + WRITE_CONCERN_ERROR, + MONGO_SERVER_ERROR, ]; export const MDBE_AUTHN_FAILED = 18; export const MDBE_AUTHZ_FAILED = 13; diff --git a/lib/helpers.js b/lib/helpers.js index 447065a..3165768 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import crypto from 'node:crypto'; diff --git a/lib/index.js b/lib/index.js index e90c17a..7ca168f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as urls from './urls.js'; @@ -81,12 +81,8 @@ export async function openCollections(names) { // open the collections logger.debug('opening collections', {collections: unopened}); const collections = {}; - const {writeConcern} = _db.options; await Promise.all(unopened.map(async name => { - // Note: We only pass `{writeConcern}` here to get around a bug in mongodb - // node driver 3.6.4 where `writeConcern` from `db` is not passed to - // collection - collections[name] = await _db.collection(name, {writeConcern}); + collections[name] = await _db.collection(name); })); // merge results into collection cache @@ -115,9 +111,6 @@ export async function createIndexes(options) { /** * Creates a streaming GridFS bucket instance. * - * By default the writeOptions config value is used for the GridFSBucket - * writeConcern option. - * * @param {object} options - See GridFSBucket documentation. * * @returns {object} The new GridFSBucket instance. diff --git a/lib/logger.js b/lib/logger.js index d47a292..a84c199 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2017-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2017-2024 Digital Bazaar, Inc. All rights reserved. */ import {loggers} from '@bedrock/core'; export const logger = loggers.get('app').child('bedrock-mongodb'); diff --git a/lib/test.config.js b/lib/test.config.js index a7fc560..a85bd2f 100644 --- a/lib/test.config.js +++ b/lib/test.config.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ import {config} from '@bedrock/core'; diff --git a/lib/urls.js b/lib/urls.js index c6d0224..e420d0d 100644 --- a/lib/urls.js +++ b/lib/urls.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2012-2024 Digital Bazaar, Inc. All rights reserved. */ export function create(config) { let url = `${config.protocol}://`; diff --git a/package.json b/package.json index ad4c71f..85e61de 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "homepage": "https://github.com/digitalbazaar/bedrock-mongodb", "dependencies": { "klona": "^2.0.5", - "mongodb": "^3.7.3", + "mongodb": "^6.3.0", "semver": "^7.3.7" }, "peerDependencies": { diff --git a/test/mocha/10-api.js b/test/mocha/10-api.js index a48fdde..88698da 100644 --- a/test/mocha/10-api.js +++ b/test/mocha/10-api.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2017-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2017-2024 Digital Bazaar, Inc. All rights reserved. */ import * as database from '@bedrock/mongodb'; @@ -406,6 +406,26 @@ describe('api', function() { } should.exist(result); should.not.exist(error); + result.should.have.keys(['acknowledged', 'insertedId']); + }); + it('should properly promote binary values to buffers', async function() { + let error; + let result = null; + const recordId = '06f336c0-7177-401b-a8ce-9a2e36331b8e'; + try { + const record = { + id: recordId, + aBinaryField: Buffer.from(recordId) + }; + await database.collections.test.insertOne(record); + result = await database.collections.test.findOne({id: recordId}); + } catch(e) { + error = e; + } + should.not.exist(error); + should.exist(result); + result.aBinaryField.should.be.instanceof(Buffer); + result.aBinaryField.toString().should.equal(recordId); }); it('should insertMany into a collection', async function() { let error; @@ -455,4 +475,27 @@ describe('api', function() { result.length.should.equal(2); }); }); + describe('isDuplicateError helper', () => { + it('should properly detect a duplicate error', async function() { + await database.openCollections(['test']); + await database.createIndexes([{ + collection: 'test', + fields: {id: 1}, + options: {unique: true} + }]); + const record = { + id: 'f466586f-7006-474d-ae44-d16c96a7b5c3' + }; + await database.collections.test.insertOne(record); + + let error = null; + try { + await database.collections.test.insertOne(record); + } catch(e) { + error = e; + } + should.exist(error); + database.isDuplicateError(error).should.be.true; + }); + }); }); diff --git a/test/test.config.js b/test/test.config.js index 7c4be75..c4dddf2 100644 --- a/test/test.config.js +++ b/test/test.config.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved. */ import {config} from '@bedrock/core'; import {fileURLToPath} from 'node:url'; diff --git a/test/test.js b/test/test.js index cff4004..6761355 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import '@bedrock/mongodb';