UNPKG

atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

373 lines (329 loc) 9 kB
const Sequelize = require("sequelize"); const { getAsObject } = require("./utils"); /** * Make sure we accept JugglingDB style opts (e.g. opts.type). * * This is mostly to allow for old code written with ACE to continue to work with Sequelize. * @param opts the raw opts object * @returns {Object} the Sequelize style opts object */ function toSequelizeOpts(opts) { if (opts.type) { if (opts.type === "memory") { opts.dialect = "sqlite"; opts.storage = ":memory:"; } else { opts.dialect = opts.type; } delete opts.type; } return opts; } class SequelizeAdapter { constructor(logger, opts) { const self = this; let sequelize; opts = toSequelizeOpts(opts); opts.logging = opts.logging !== false ? logger.info : false; if (opts.dialect && opts.dialect === "sqlite" && opts.storage) { sequelize = self.schema = new Sequelize(null, null, null, opts); } else { const sequelizeOpts = { logging: opts.logging, pool: opts.pool }; if (opts.dialectOptions) { sequelizeOpts.dialectOptions = opts.dialectOptions; } sequelize = self.schema = new Sequelize( process.env["DB_URL"] || opts.url, sequelizeOpts ); } const AddonSettings = sequelize.define( opts.table || "AddonSetting", { clientKey: { type: Sequelize.STRING, allowNull: true }, key: { type: Sequelize.STRING, allowNull: true }, val: { type: Sequelize.JSON, allowNull: true } }, { indexes: [ { fields: ["clientKey", "key"] } ], timestamps: false } ); const InstallationClientKey = sequelize.define( "InstallationClientKey", { installationId: { type: Sequelize.STRING, allowNull: false }, clientKey: { type: Sequelize.STRING, allowNull: false } }, { indexes: [ { fields: ["installationId"] }, { fields: ["clientKey"] } ], timestamps: false } ); const ForgeSettings = sequelize.define( "ForgeSetting", { installationId: { type: Sequelize.STRING, allowNull: false }, key: { type: Sequelize.STRING, allowNull: false }, val: { type: Sequelize.JSON, allowNull: false } }, { indexes: [ { fields: ["installationId", "key"] } ], timestamps: false } ); AddonSettings.hasMany(InstallationClientKey, { as: "InstallationClientKey", foreignKey: "clientKey", sourceKey: "clientKey", constraints: false }); InstallationClientKey.belongsTo(AddonSettings, { foreignKey: "clientKey", constraints: false }); this.connectionPromise = AddonSettings.sync(); this.installationMappingPromise = InstallationClientKey.sync(); this.forgeSettingsPromise = ForgeSettings.sync(); this.settings = { logging: opts.logging, dialect: opts.dialect, storage: opts.storage, table: opts.table }; this.sequelize = sequelize; } isMemoryStore() { const options = this.schema.options; return options.storage === ":memory:"; } // run a query with an arbitrary 'where' clause // returns an array of values async _get(where) { const settings = await this.connectionPromise; const results = await settings.findAll({ where }); return results.map(result => { return getAsObject(result.get("val")); }); } getAllClientInfos() { return this._get({ key: "clientInfo" }); } // return a promise to a single object identified by 'key' in the data belonging to tenant 'clientKey' async get(key, clientKey) { const settings = await this.connectionPromise; const result = await settings.findOne({ where: { key, clientKey } }); return result ? getAsObject(result.get("val")) : null; } async saveInstallation(value, clientKey) { return this.sequelize.transaction(async t => { const clientSetting = await this.set("clientInfo", value, clientKey, t); const forgeInstallationId = clientSetting.installationId; if (forgeInstallationId) { await this.associateInstallations(forgeInstallationId, clientKey, t); } return clientSetting; }); } async set(key, value, clientKey, transaction) { const settings = await this.connectionPromise; // TODO Investigate using upsert for brevity: // https://community.developer.atlassian.com/t/i-found-a-bug-in-atlassian-connect-express-sequelize-storage-adapter-how-do-i-report-it/42399 // https://ecosystem.atlassian.net/browse/ACEJS-161 const [result, created] = await settings.findOrCreate({ where: { key, clientKey }, defaults: { val: value }, transaction }); if (created) { return getAsObject(result.get("val")); } const updatedModel = await result.update( { val: value }, { transaction } ); return getAsObject(updatedModel.get("val")); } async del(key, clientKey) { let whereClause; if (arguments.length < 2) { whereClause = { clientKey: key }; } else { whereClause = { key, clientKey }; } const settings = await this.connectionPromise; return settings.destroy({ where: whereClause }); } /* Storage supports for handling Forge installationId */ async getClientSettingsForForgeInstallation(forgeInstallationId) { const dbConnections = Promise.all([ this.connectionPromise, this.installationMappingPromise ]); const [clientSettings, installationMappings] = await dbConnections; // for LEFT OUTER join: https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/#complex-where-clauses-at-the-top-level const results = await clientSettings.findAll({ where: { "$InstallationClientKey.installationId$": { [Sequelize.Op.eq]: forgeInstallationId }, "$AddonSetting.key$": { [Sequelize.Op.eq]: "clientInfo" } }, include: [ { model: installationMappings, as: "InstallationClientKey" } ] }); return results.length ? getAsObject(results[0].get("val")) : null; } async associateInstallations(forgeInstallationId, clientKey, transaction) { const installationMapping = await this.installationMappingPromise; const [result, created] = await installationMapping.findOrCreate({ where: { installationId: forgeInstallationId }, defaults: { clientKey }, transaction }); if (created) { return created; } const updatedModel = await result.update( { clientKey }, { transaction } ); return updatedModel; } async deleteAssociation(forgeInstallationId) { const installationMapping = await this.installationMappingPromise; return installationMapping.destroy({ where: { installationId: forgeInstallationId } }); } // Storage interface for Forge settings forForgeInstallation(installationId) { return { del: async key => { const forgeSettings = await this.forgeSettingsPromise; return forgeSettings.destroy({ where: { key, installationId } }); }, get: async key => { const forgeSettings = await this.forgeSettingsPromise; const result = await forgeSettings.findOne({ where: { key, installationId } }); return result ? getAsObject(result.get("val")) : null; }, set: async (key, val) => { const forgeSettings = await this.forgeSettingsPromise; const result = await this.sequelize.transaction(async transaction => { const [result, created] = await forgeSettings.findOrCreate({ where: { key, installationId }, defaults: { val }, transaction }); if (created) { return getAsObject(result.get("val")); } const updatedModel = await result.update({ val }, { transaction }); return getAsObject(updatedModel.get("val")); }); return result; } }; } } module.exports = function (logger, opts) { if (arguments.length === 0) { return SequelizeAdapter; } return new SequelizeAdapter(logger, opts); };