atlassian-connect-express
Version:
Library for building Atlassian Add-ons on top of Express
373 lines (329 loc) • 9 kB
JavaScript
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);
};