Skip to content

Commit d40ff66

Browse files
add support for CSFLE/QE for all primitive types
1 parent 7672ade commit d40ff66

File tree

5 files changed

+872
-635
lines changed

5 files changed

+872
-635
lines changed

lib/collection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Collection.prototype.onOpen = function() {
8181
* @api private
8282
*/
8383

84-
Collection.prototype.onClose = function() {};
84+
Collection.prototype.onClose = function() { };
8585

8686
/**
8787
* Queues a method for later execution when its

lib/connection.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
607607

608608
Connection.prototype.createCollections = async function createCollections(options = {}) {
609609
const result = {};
610-
const errorsMap = { };
610+
const errorsMap = {};
611611

612612
const { continueOnError } = options;
613613
delete options.continueOnError;
@@ -734,7 +734,7 @@ Connection.prototype.transaction = function transaction(fn, options) {
734734
throw err;
735735
}).
736736
finally(() => {
737-
session.endSession().catch(() => {});
737+
session.endSession().catch(() => { });
738738
});
739739
});
740740
};
@@ -1025,7 +1025,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10251025

10261026
for (const model of Object.values(this.models)) {
10271027
// Errors handled internally, so safe to ignore error
1028-
model.init().catch(function $modelInitNoop() {});
1028+
model.init().catch(function $modelInitNoop() { });
10291029
}
10301030

10311031
// `createConnection()` calls this `openUri()` function without
@@ -1061,7 +1061,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10611061
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10621062
Connection.prototype.on = function on(event, callback) {
10631063
if (event === 'error' && this.$initialConnection) {
1064-
this.$initialConnection.catch(() => {});
1064+
this.$initialConnection.catch(() => { });
10651065
}
10661066
return EventEmitter.prototype.on.call(this, event, callback);
10671067
};
@@ -1083,7 +1083,7 @@ Connection.prototype.on = function on(event, callback) {
10831083
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10841084
Connection.prototype.once = function on(event, callback) {
10851085
if (event === 'error' && this.$initialConnection) {
1086-
this.$initialConnection.catch(() => {});
1086+
this.$initialConnection.catch(() => { });
10871087
}
10881088
return EventEmitter.prototype.once.call(this, event, callback);
10891089
};
@@ -1412,7 +1412,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14121412
}
14131413

14141414
// Errors handled internally, so safe to ignore error
1415-
model.init().catch(function $modelInitNoop() {});
1415+
model.init().catch(function $modelInitNoop() { });
14161416

14171417
return model;
14181418
}
@@ -1439,7 +1439,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14391439
}
14401440

14411441
if (this === model.prototype.db
1442-
&& (!collection || collection === model.collection.name)) {
1442+
&& (!collection || collection === model.collection.name)) {
14431443
// model already uses this connection.
14441444

14451445
// only the first model with this name is cached to allow
@@ -1626,8 +1626,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanis
16261626
*/
16271627
Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
16281628
return (options) &&
1629-
(options.user) &&
1630-
((options.pass) || this.authMechanismDoesNotRequirePassword());
1629+
(options.user) &&
1630+
((options.pass) || this.authMechanismDoesNotRequirePassword());
16311631
};
16321632

16331633
/**
@@ -1689,7 +1689,7 @@ Connection.prototype.createClient = function createClient() {
16891689
*/
16901690
Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
16911691
const result = {};
1692-
const errorsMap = { };
1692+
const errorsMap = {};
16931693

16941694
const { continueOnError } = options;
16951695
delete options.continueOnError;

lib/drivers/node-mongodb-native/connection.js

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const pkg = require('../../../package.json');
1212
const processConnectionOptions = require('../../helpers/processConnectionOptions');
1313
const setTimeout = require('../../helpers/timers').setTimeout;
1414
const utils = require('../../utils');
15+
const { inferBSONType } = require('../../encryption_utils');
16+
const clone = require('../../helpers/clone');
1517

1618
/**
1719
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -304,6 +306,9 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
304306
};
305307
}
306308

309+
310+
options = this._buildEncryptionSchemas(options);
311+
307312
this.readyState = STATES.connecting;
308313
this._connectionString = uri;
309314

@@ -327,6 +332,86 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
327332
return this;
328333
};
329334

335+
/**
336+
* Given a connection, which may or may not have encrypted models, build
337+
* a schemaMap and/or an encryptedFieldsMap for the connection, combining all models
338+
* into a single schemaMap and encryptedFields map.
339+
*
340+
* @param { object } options
341+
* @param { object } [options.autoEncryption] the auto encryption options for the connection. Technically
342+
* marked optional, because it is optional in the MongoClient options interface, but auto encryption
343+
* will fail without it so in practice it is always required if encryption is enabled.
344+
*
345+
* @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption
346+
* options.
347+
*/
348+
NativeConnection.prototype._buildEncryptionSchemas = function(options) {
349+
const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce(
350+
schemaMapReducer.bind(this),
351+
{}
352+
);
353+
const encryptedFieldsMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'qe').reduce(
354+
encryptedFieldsMapReducer.bind(this),
355+
{}
356+
);
357+
358+
return Object.assign(
359+
clone(options), {
360+
autoEncryption: {
361+
...options.autoEncryption,
362+
schemaMap,
363+
encryptedFieldsMap
364+
}
365+
});
366+
367+
/**
368+
* @param {object} schemaMap the accumulation schemaMap
369+
* @param {Model} the model
370+
* @returns
371+
*/
372+
function schemaMapReducer(schemaMap, model) {
373+
const { schema, collection: { collectionName } } = model;
374+
const namespace = `${this.$dbName}.${collectionName}`;
375+
376+
function schemaMapPropertyReducer(accum, [path, propertyConfig]) {
377+
const bsonType = inferBSONType(schema, path);
378+
// `<db>.<collection>`: { encrypt: { keyId, bsonType, algorithm }}
379+
accum[path] = { encrypt: { ...propertyConfig, bsonType } };
380+
return accum;
381+
}
382+
const properties = Object.entries(schema.encryptedFields).reduce(
383+
schemaMapPropertyReducer,
384+
{});
385+
386+
schemaMap[namespace] = {
387+
bsonType: 'object',
388+
properties
389+
};
390+
391+
return schemaMap;
392+
}
393+
394+
/**
395+
*
396+
* @param {object} encryptedFieldsMap the accumulation encryptedFieldsMap
397+
* @param {Model} the model
398+
* @returns
399+
*/
400+
function encryptedFieldsMapReducer(encryptedFieldsMap, { schema, collection: { collectionName } }) {
401+
const namespace = `${this.$dbName}.${collectionName}`;
402+
const fields = Object.entries(schema.encryptedFields).map(
403+
([path, config]) => {
404+
const bsonType = inferBSONType(schema, path);
405+
// { path, bsonType, keyId, queries? }
406+
return { path, bsonType, ...config };
407+
});
408+
409+
encryptedFieldsMap[namespace] = { fields };
410+
411+
return encryptedFieldsMap;
412+
}
413+
};
414+
330415
/*!
331416
* ignore
332417
*/
@@ -347,7 +432,7 @@ NativeConnection.prototype.setClient = function setClient(client) {
347432

348433
for (const model of Object.values(this.models)) {
349434
// Errors handled internally, so safe to ignore error
350-
model.init().catch(function $modelInitNoop() {});
435+
model.init().catch(function $modelInitNoop() { });
351436
}
352437

353438
return this;
@@ -390,9 +475,9 @@ function _setClient(conn, client, options, dbName) {
390475
};
391476

392477
const type = client &&
393-
client.topology &&
394-
client.topology.description &&
395-
client.topology.description.type || '';
478+
client.topology &&
479+
client.topology.description &&
480+
client.topology.description.type || '';
396481

397482
if (type === 'Single') {
398483
client.on('serverDescriptionChanged', ev => {

0 commit comments

Comments
 (0)