import 'regenerator-runtime/runtime';
/** Class implementing the Apollo Passport DBDriver interface */
class RethinkDBDashDriver {
/**
* 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 {rethinkdbdash} r, e.g. var r = require('rethinkdbdash')();
*
* @param {string} options.userTableName default: 'users'
* @param {string} options.configTableName default: 'apolloPassportConfig'
* @param {string} options.dbName default: current database
*/
constructor(r, options = {}) {
this.r = r;
this.userTableName = options.userTableName || 'users';
this.configTableName = options.configTableName || 'apolloPassportConfig';
this.dbName = options.dbName || (r._poolMaster && r._poolMaster._options.db);
this.db = r.db(this.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() {
await this._assertTableExists(this.userTableName);
this.users = this.db.table(this.userTableName);
await this._assertTableExists(this.configTableName);
this.config = this.db.table(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);
});
}
//////////////////
// DB UTILITIES //
//////////////////
/**
* Internal method, documented for benefit of driver authors. Asserts (and
* awaits) that the given table name exists. This is a convenience for the
* user but with RethinkDB **there is no safe way to do this** other than
* creating the table in advance (outside of the app). It's fine if the
* table is created with only one app instance running, which is usually
* the case for initial setup.
*
* @param {string} name - the name of the table to assert
*/
async _assertTableExists(name) {
try {
await this.db.tableCreate(name).run();
} catch (err) {
if (err.msg !== `Table \`${this.dbName}.${name}\` already exists.`) {
throw err;
}
// XXX give a warning about the caveat in the docs above.
}
}
//////////////////
// 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.run();
const out = {};
results.forEach(row => {
if (!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.insert({ 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();
let id = user.id;
const result = await this.users.insert(user);
if (!id)
id = result.generated_keys[0];
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.get(userId).run();
}
/**
* 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
.filter(this.r.row('emails').contains(row => row('address').eq(email)))
.limit(1)
.run();
return results[0] || 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.filter(
this.r.or(
this.r.row('services')(service)('id').eq(id).default(false),
this.r.row('emails').contains({ address: email }).default(false)
)
).limit(1).run();
return results[0] || 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();
await this.users.get(userId).update(row => ({
emails: row('emails').default([])
.filter(row('emails').default([]).contains({ address: email }).not())
.append({ address: email, ...data })
}));
}
/**
* 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.get(userId).update({ 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 RethinkDBDashDriver;