UNPKG

atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

332 lines (299 loc) 8.45 kB
const _ = require("lodash"); const MongoClient = require("mongodb").MongoClient; const { getAsObject } = require("./utils"); const CONNECTOR_OPTIONS = ["rs", "url", "collection", "database", "adapter"]; class MongoDbAdapter { constructor(logger, opts) { const self = this; _.bindAll( self, "get", "set", "del", "isMemoryStore", "getAllClientInfos", "_get", "_mongoDbConnected", "saveInstallation", "associateInstallations", "deleteAssociation", "getClientSettingsForForgeInstallation" ); self.mongoDbClient = null; self.db = null; self.collection = null; self.settings = { // Backwards compatible way of determining connectionUrl connectionUrl: process.env["MONGODB_URI"] || process.env["DB_URL"] || opts.rs || opts.url, collectionName: opts.collection || "AddonSettings", databaseName: opts.database || undefined, // undefined means default to the one from connectionUrl // Prepare mongodb options mongoDbOpts: _.assign( { retryWrites: false, useNewUrlParser: true, useUnifiedTopology: true }, opts ) }; // Eliminate all options in opts for self class and pass along the rest to mongoclient CONNECTOR_OPTIONS.forEach( (optName => { delete self.settings.mongoDbOpts[optName]; }).bind(self) ); const client = new MongoClient( self.settings.connectionUrl, self.settings.mongoDbOpts ); self.connectionPromise = client .connect() .then(self._mongoDbConnected) .catch(err => { (logger || console).error( `Could not establish MongoDB database connection: ${err.toString()}` ); return Promise.reject(err); }); } isMemoryStore() { // Even if the mongoDB server was operating in in-memory mode, it’s not in the node process memory and won’t // disappear when the node service restarts, so from our perspective it’s still not an in-memory store return false; } async _mongoDbConnected(mongoDbClient) { const self = this; self.mongoDbClient = mongoDbClient; self.db = self.mongoDbClient.db(self.settings.databaseName); self.collection = self.db.collection(self.settings.collectionName); self.installationClientKeysCollection = self.db.collection( "InstallationClientKeys" ); self.forgeCollection = self.db.collection("ForgeSettings"); return Promise.all([ self.collection.createIndex({ key: 1, clientKey: 1 }, { unique: true }), self.collection.createIndex({ key: 1 }), self.installationClientKeysCollection.createIndex({ installationId: 1 }), self.forgeCollection.createIndex( { installationId: 1, key: 1 }, { unique: true } ) ]); } // run a query with an arbitrary 'where' clause // returns an array of values _get(where) { const self = this; return self.connectionPromise .then(() => { return self.collection.find(where).toArray(); }) .then(resultsArray => { if ( !resultsArray || !Array.isArray(resultsArray) || resultsArray.length === 0 ) { return []; } return resultsArray.map(entry => { return entry.val; }); }); } getAllClientInfos() { const self = this; return self._get({ key: "clientInfo" }); } // return a promise to a single object identified by 'key' in the data belonging to tenant 'clientKey' get(key, clientKey) { const self = this; if (typeof key !== "string") { return Promise.reject( new Error("The key for what to get in MongoDB must be a string") ); } if (typeof clientKey !== "string") { return Promise.reject( new Error("The clientKey for what to get in MongoDB must be a string") ); } return self.connectionPromise .then(() => { return self.collection.findOne({ key, clientKey }); }) .then(resultDoc => { if (resultDoc) { return resultDoc.val; } else { return null; } }); } async saveInstallation(val, clientKey) { await this.connectionPromise; const clientSetting = await this.set("clientInfo", val, clientKey); const forgeInstallationId = clientSetting.installationId; if (forgeInstallationId) { await this.associateInstallations(forgeInstallationId, clientKey); } return clientSetting; } set(key, value, clientKey) { const self = this; if (typeof key !== "string") { return Promise.reject( new Error("The key for what to set in MongoDB must be a string") ); } if (typeof clientKey !== "string") { return Promise.reject( new Error("The clientKey for what to set in MongoDB must be a string") ); } value = getAsObject(value); return self.connectionPromise .then(() => { return self.collection.replaceOne( { key, clientKey }, { key, clientKey, val: value }, { upsert: true } ); }) .then(() => { return value; }); } del(key, clientKey) { const self = this; let query; if (clientKey === undefined) { // try to interpret key as clientKey query = { clientKey: key }; } else { query = { key, clientKey }; } // Type checks if (typeof query.clientKey !== "string") { return Promise.reject( new Error( "The clientKey for what to delete from MongoDB must be a string" ) ); } if (query.key !== undefined && typeof query.key !== "string") { return Promise.reject( new Error( "The key for what to delete from MongoDB must be a string (or undefined if you " + "want to delete all entries with the given clientKey)" ) ); } return self.connectionPromise.then(() => { return self.collection.deleteMany(query); }); } async associateInstallations(forgeInstallationId, clientKey) { await this.connectionPromise; const query = await this.installationClientKeysCollection.replaceOne( { installationId: forgeInstallationId }, { installationId: forgeInstallationId, clientKey }, { upsert: true } ); return query.acknowledged; } async deleteAssociation(forgeInstallationId) { await this.connectionPromise; const query = await this.installationClientKeysCollection.deleteOne({ installationId: forgeInstallationId }); return query.nRemoved > 0; } /* Storage supports for handling Forge installationId */ async getClientSettingsForForgeInstallation(forgeInstallationId) { await this.connectionPromise; const document = await this.installationClientKeysCollection.findOne({ installationId: forgeInstallationId }); if (!document) { return null; } const val = await this.get("clientInfo", document.clientKey); if (!val) { return null; } return getAsObject(val); } // Storage interface for Forge settings forForgeInstallation(installationId) { const self = this; return { del: async key => { await self.connectionPromise; return self.forgeCollection.deleteOne({ key, installationId }); }, get: async key => { await self.connectionPromise; const result = await self.forgeCollection.findOne({ key, installationId }); return result ? result.val : null; }, set: async (key, val) => { await self.connectionPromise; await self.forgeCollection.replaceOne( { key, installationId }, { key, val, installationId }, { upsert: true } ); return val; } }; } } module.exports = function (logger, opts) { if (0 === arguments.length) { return MongoDbAdapter; } return new MongoDbAdapter(logger, opts); };