apollo-passport-mongodb-driver
Version:
mongodb driver for apollo-passport
581 lines (490 loc) • 19.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
require('regenerator-runtime/runtime');
var _mongodb = require('mongodb');
var _mongodb2 = _interopRequireDefault(_mongodb);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var ObjectId = _mongodb2.default.ObjectId;
/** Class implementing the Apollo Passport DBDriver interface */
var MongoDbDriver = function () {
/**
* Returns a DBDriver instance (for use by Apollo Passport). Parameters are
* driver-specific and should be clearly specificied in the README.
* This documents the RethinkDBDash DBDriver specifically, although some
* *options* are relevant for all drivers.
*
* @param {db} mongo instance, e.g. MongoClient.connect(url, function(err, db) { ... db });
*
* @param {string} options.userTableName default: 'users'
* @param {string} options.configTableName default: 'apolloPassportConfig'
* @param {string} options.dbName default: current database
*/
function MongoDbDriver(db) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, MongoDbDriver);
this.db = db;
this.userTableName = options.userTableName || 'users';
this.configTableName = options.configTableName || 'apolloPassportConfig';
this.dbName = options.dbName;
this.readySubs = [];
// don't await the init, run async
if (options.init !== false) this._init();
}
/**
* Internal method, documented for benefit of driver authors. Most important
* is to call fetchConfig() (XXX unfinished), but may also assert that all
* tables exist, and run ready callbacks.
*/
_createClass(MongoDbDriver, [{
key: '_init',
value: function _init() {
return regeneratorRuntime.async(function _init$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
this.users = this.db.collection(this.userTableName);
this.config = this.db.collection(this.configTableName);
this.initted = true;
while (this.readySubs.length) {
this.readySubs.shift().call();
}
case 4:
case 'end':
return _context.stop();
}
}
}, null, this);
}
/**
* Internal method, documented for benefit of driver authors. An awaitable
* promise that returns if the driver is ready (or when it becomes ready).
*/
}, {
key: '_ready',
value: function _ready() {
var _this = this;
return new Promise(function (resolve) {
if (_this.initted) resolve();else _this.readySubs.push(resolve);
});
}
//////////////////
// CONFIG TABLE //
//////////////////
/**
* Retrieves _all_ configuration from the database.
* @return {object} A nested dictionary arranged by type, i.e.
*
* ```js
* {
* service: { // type
* facebook: { // id
* ...data // value (de-JSONified if from non-document DB)
* }
* }
* }
* ```
*/
}, {
key: 'fetchConfig',
value: function fetchConfig() {
var results, out;
return regeneratorRuntime.async(function fetchConfig$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context2.next = 4;
return regeneratorRuntime.awrap(this.config.find().toArray());
case 4:
results = _context2.sent;
out = {};
results.forEach(function (row) {
if (!out[row.type]) out[row.type] = {};
out[row.type][row._id] = row;
});
return _context2.abrupt('return', out);
case 8:
case 'end':
return _context2.stop();
}
}
}, null, this);
}
/**
* Creates or updates the key with the given value.
* NoSQL databases can store the destructured value as part of the record.
* Fixed-schema databases should JSON-encode the 'value' column.
*
* @param {string} type - e.g. "service"
* @param {string} id - e.g. "facebook"
* @param {object} value - e.g. { id: 1, ...profile }
*/
}, {
key: 'setConfigKey',
value: function setConfigKey(type, _id, value) {
return regeneratorRuntime.async(function setConfigKey$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context3.next = 4;
return regeneratorRuntime.awrap(this.config.insertOne(_extends({ type: type, _id: _id }, value)));
case 4:
case 'end':
return _context3.stop();
}
}
}, null, this);
}
///////////
// USERS //
///////////
/**
* Given a user record, save it to the database, and return its given id.
* NoSQL databases should store the entire object, schema-based databases
* should honor the 'emails' and 'services' keys and store as necessary
* in another table.
*
* @param {object} user
*
* Emails format follow this standard:
* http://passportjs.org/docs/profile
* {
* emails: [ { value: "me@me.com" } ],
* services: [ { facebook: { id: 1, ...profile } } ]
* ...anyOtherDataForUserRecordAtCreationTimeFromAppHooks
* }
*
* @return {string} the id of the inserted user record
*/
}, {
key: 'createUser',
value: function createUser(user) {
var id;
return regeneratorRuntime.async(function createUser$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
_context4.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
if (!user._id) {
user._id = new ObjectId().toString();
}
user.dateUpdated = new Date();
user.dateAdded = new Date();
user.dateRegistered = new Date();
id = user._id;
_context4.next = 9;
return regeneratorRuntime.awrap(this.users.insertOne(user));
case 9:
return _context4.abrupt('return', id);
case 10:
case 'end':
return _context4.stop();
}
}
}, null, this);
}
}, {
key: 'updateUser',
value: function updateUser(userId, updatedUser) {
var result;
return regeneratorRuntime.async(function updateUser$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
_context5.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context5.next = 4;
return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: updatedUser,
$currentDate: {
dateUpdated: true
} }));
case 4:
result = _context5.sent;
return _context5.abrupt('return', result.result.ok);
case 6:
case 'end':
return _context5.stop();
}
}
}, null, this);
}
/**
* Fetches a user record by id. Schema-based databases should merge
* appropriate user-data from e.g. `user_emails` and `user_services`.
*
* @param {string} id - the user record's id
*
* @return {object} user object in the same format expected by
* {@link RethinkDBDashDriver#createUser}, or *null* if none found.
*/
}, {
key: 'fetchUserById',
value: function fetchUserById(userId) {
return regeneratorRuntime.async(function fetchUserById$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
_context6.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
return _context6.abrupt('return', this.users.findOne({ _id: userId }));
case 3:
case 'end':
return _context6.stop();
}
}
}, null, this);
}
/**
* Given a single "email" param, returns the matching user record if one
* exists, or null, otherwise.
*
* @param {string} email - the email address to search for, e.g. "me@me.com"
*
* @return {object} user object in the same format expected by
* {@link RethinkDBDashDriver#createUser}, or *null* if none found.
*/
}, {
key: 'fetchUserByEmail',
value: function fetchUserByEmail(email) {
var results;
return regeneratorRuntime.async(function fetchUserByEmail$(_context7) {
while (1) {
switch (_context7.prev = _context7.next) {
case 0:
_context7.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context7.next = 4;
return regeneratorRuntime.awrap(this.users.findOne({ 'emails.value': email }));
case 4:
results = _context7.sent;
return _context7.abrupt('return', results || null);
case 6:
case 'end':
return _context7.stop();
}
}
}, null, this);
}
/**
* Returns a user who has *either* a matching email address or matching
* service record, or null, otherwise.
*
* @param {string} service - name of the service, e.g. "facebook"
* @param {string} id - id of the service record, e.g. "152356242"
* @param {string} email - the email address to search for, e.g. "me@me.com"
*
* @return {object} user object in the same format expected by
* {@link RethinkDBDashDriver#createUser}, or *null* if none found
*/
}, {
key: 'fetchUserByServiceIdOrEmail',
value: function fetchUserByServiceIdOrEmail(service, id, email) {
var results;
return regeneratorRuntime.async(function fetchUserByServiceIdOrEmail$(_context8) {
while (1) {
switch (_context8.prev = _context8.next) {
case 0:
_context8.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context8.next = 4;
return regeneratorRuntime.awrap(this.users.findOne({ $or: [_defineProperty({}, 'services.' + service + '.id', id), { 'emails.value': email }] }));
case 4:
results = _context8.sent;
return _context8.abrupt('return', results || null);
case 6:
case 'end':
return _context8.stop();
}
}
}, null, this);
}
/**
* Given a userId, set the verified field as true, and delete the verification tokens
*
* @param {string} userId - the id of the user to assert
* @param {string} verifiedField - name of the verify field to change to true
* @param {string} tokenField - name of the token field to delete
* @param {string} tokenExpirationField - name of the token expiration field to delete
*
*/
}, {
key: 'verifyUserAccount',
value: function verifyUserAccount(userId) {
var verifiedField = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'verified';
var _$unset;
var tokenField = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'verificationToken';
var tokenExpirationField = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'verificationTokenExpiration';
var user;
return regeneratorRuntime.async(function verifyUserAccount$(_context9) {
while (1) {
switch (_context9.prev = _context9.next) {
case 0:
_context9.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context9.next = 4;
return regeneratorRuntime.awrap(this.users.findOne({ _id: userId }));
case 4:
user = _context9.sent;
_context9.next = 7;
return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, { $set: _defineProperty({}, verifiedField, true),
$unset: (_$unset = {}, _defineProperty(_$unset, tokenField, ''), _defineProperty(_$unset, tokenExpirationField, ''), _$unset),
$currentDate: {
dateUpdated: true
}
}));
case 7:
case 'end':
return _context9.stop();
}
}
}, null, this);
}
}, {
key: 'addResetPasswordToken',
value: function addResetPasswordToken(userId, token, tokenExpiration) {
var _$set2;
var tokenField = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'resetPassToken';
var tokenExpirationField = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'resetPassTokenExpiration';
return regeneratorRuntime.async(function addResetPasswordToken$(_context10) {
while (1) {
switch (_context10.prev = _context10.next) {
case 0:
_context10.next = 2;
return regeneratorRuntime.awrap(this.users.updateOne({
_id: userId }, {
$set: (_$set2 = {}, _defineProperty(_$set2, tokenField, token), _defineProperty(_$set2, tokenExpirationField, tokenExpiration), _$set2),
$currentDate: {
dateUpdated: true
},
// Remove the regular verification token, because if the user reset his password
// from the email, we are sure that this his email
// We don't want both token to live together
$unset: {
verificationToken: '',
tokenExpirationField: ''
}
}));
case 2:
case 'end':
return _context10.stop();
}
}
}, null, this);
}
/**
* Given a userId, ensures the user record contains the given email
* address, and updates it with optional data.
*
* @param {string} userId - the id of the user to assert
* @param {string} email - the email address to ensure exists
* @param {object} data - optional, e.g. { type: 'work', verified: true }
*/
}, {
key: 'assertUserEmailData',
value: function assertUserEmailData(userId, email, data) {
var user, userEmail, emailData, idx, _emailData;
return regeneratorRuntime.async(function assertUserEmailData$(_context11) {
while (1) {
switch (_context11.prev = _context11.next) {
case 0:
_context11.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context11.next = 4;
return regeneratorRuntime.awrap(this.users.findOne({ _id: userId }));
case 4:
user = _context11.sent;
userEmail = user.emails.find(function (e) {
return e.value === email;
});
if (userEmail) {
_context11.next = 10;
break;
}
emailData = _extends({ value: email }, data);
_context11.next = 10;
return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, {
$push: { 'emails': emailData },
$currentDate: {
dateUpdated: true
} }));
case 10:
if (!data) {
_context11.next = 15;
break;
}
idx = user.emails.indexOf(userEmail);
_emailData = _extends({}, userEmail, data);
_context11.next = 15;
return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, {
$set: _defineProperty({}, 'emails.' + idx, _emailData),
$currentDate: {
dateUpdated: true
} }));
case 15:
case 'end':
return _context11.stop();
}
}
}, null, this);
}
/**
* Given a userId, ensure the user record contains the given service
* record, and updates it with the given data.
*
* @param {string} userId - the id of the user to assert
* @param {string} service - the name of the service, e.g. "facebook"
* @param {object} data - e.g. { id: "4321", displayName: "John Sheppard" }
*/
}, {
key: 'assertUserServiceData',
value: function assertUserServiceData(userId, service, data) {
return regeneratorRuntime.async(function assertUserServiceData$(_context12) {
while (1) {
switch (_context12.prev = _context12.next) {
case 0:
_context12.next = 2;
return regeneratorRuntime.awrap(this._ready());
case 2:
_context12.next = 4;
return regeneratorRuntime.awrap(this.users.updateOne({ _id: userId }, {
$set: { services: _defineProperty({}, service, _extends({}, data)) },
$currentDate: {
dateUpdated: true
} }));
case 4:
case 'end':
return _context12.stop();
}
}
}, null, this);
}
// Not sure if we need this anymore, since fetch*() functions return
// normalized data. But let's see.
}, {
key: 'mapUserToServiceData',
value: function mapUserToServiceData(user, service) {
return user && user.services && user.services[service];
}
}]);
return MongoDbDriver;
}();
exports.default = MongoDbDriver;