All files index.js

96.43% Statements 27/28
95.24% Branches 20/21
100% Functions 4/4
96.43% Lines 27/28
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 2341x 1x                                 12x 6x 6x 6x 6x 6x     6x 5x                 5x 5x   5x   5x               25x 25x 25x 24x   1x                                                       1x 1x 1x   1x                                                                                   2x 1x                                                                                                                                           2x                                                         1x          
import 'regenerator-runtime/runtime';
import Random from 'meteor-random';
 
/** Class implementing the Apollo Passport DBDriver interface */
class MongoDbDriver {
 
  /**
   * 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
   */
  constructor(db, options = {}) {
    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.
   */
  async _init() {
    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();
  }
 
  /**
   * Internal method, documented for benefit of driver authors.  An awaitable
   * promise that returns if the driver is ready (or when it becomes ready).
   */
  _ready() {
    return new Promise((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)
   *       }
   *     }
   *   }
   * ```
   */
  async fetchConfig() {
    await this._ready();
 
    const results = await this.config.find().toArray();
    const out = {};
 
    results.forEach(row => {
      Eif (!out[row.type])
        out[row.type] = {};
 
      out[row.type][row._id] = row;
    });
 
    return out;
  }
 
  /**
   * 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 }
   */
  async setConfigKey(type, _id, value) {
    await this._ready();
    await this.config.insertOne({ type, _id, ...value });
  }
 
  ///////////
  // 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: [ { address: "me@me.com" } ],
   *   services: [ { facebook: { id: 1, ...profile } } ]
   *   ...anyOtherDataForUserRecordAtCreationTimeFromAppHooks
   * }
   *
   * @return {string} the id of the inserted user record
   */
  async createUser(user) {
    await this._ready();
    if (!user._id) {
      user._id = Random.id();
    }
    let id = user._id;
    
    await this.users.insertOne(user);
  
    return id;
  }
 
  /**
   * 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.
   */
  async fetchUserById(userId) {
    await this._ready();
    return this.users.findOne({_id: userId});
  }
 
  /**
   * 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.
   */
  async fetchUserByEmail(email) {
    await this._ready();
 
    const results = await this.users.findOne({ 'emails.address': email});
 
    return results || null;
  }
 
  /**
   * 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
   */
  async fetchUserByServiceIdOrEmail(service, id, email) {
    await this._ready();
 
    const results = await this.users.findOne({ $or: [{ ['services.' + service + '.id']: id }, {'emails.address': email}]});
 
    return results || null;
  }
 
  /**
   * 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 }
   */
  async assertUserEmailData(userId, email, data) {
    await this._ready();
    const user = await this.users.findOne({_id: userId});
    const userEmail = user.emails.find((e) => e.address === email);
    
    if (!userEmail) {
      const emailData = { address: email, ...data};
      await this.users.updateOne({_id: userId }, { $push: { 'emails': emailData }});
    }
    if (data) {
      const idx = user.emails.indexOf(userEmail);
      const emailData = { ...userEmail, ...data };
      await this.users.updateOne({_id: userId }, { $set: { ['emails.' + idx]: emailData}});
    }
  }
 
  /**
   * 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" }
   */
  async assertUserServiceData(userId, service, data) {
    await this._ready();
    await this.users.updateOne({ _id: userId }, { $set: { services: { [service]: { ...data } } } });
  }
 
  // Not sure if we need this anymore, since fetch*() functions return
  // normalized data.  But let's see.
  mapUserToServiceData(user, service) {
    return user && user.services && user.services[service];
  }
}
 
export default MongoDbDriver;