atlassian-connect-express
Version:
Library for building Atlassian Add-ons on top of Express
336 lines (309 loc) • 8.19 kB
JavaScript
const {
DynamoDBClient,
GetItemCommand,
PutItemCommand,
DeleteItemCommand,
ScanCommand,
CreateTableCommand,
DescribeTableCommand,
UpdateItemCommand,
ResourceNotFoundException
} = require("@aws-sdk/client-dynamodb");
const { getAsObject, getAsString } = require("./utils");
function toDynamoDBOpts(opts) {
const dynamoDBOpts = Object.assign({}, opts);
delete dynamoDBOpts.table;
return dynamoDBOpts;
}
class DynamoDBAdapter {
InstallationClientKeyTable = "InstallationClientKeys";
ForgeSettingsTable = "ForgeSettings";
constructor(logger, opts) {
this.client = new DynamoDBClient(toDynamoDBOpts(opts));
this.table = opts.table || "AddonSettings";
this.logger = logger;
this._prepareDb();
}
async _prepareDb() {
await Promise.all([
this._createTableIfNotExists({
AttributeDefinitions: [
{
AttributeName: "clientKey",
AttributeType: "S"
},
{
AttributeName: "key",
AttributeType: "S"
}
],
KeySchema: [
{
AttributeName: "clientKey",
KeyType: "HASH"
},
{
AttributeName: "key",
KeyType: "RANGE"
}
],
BillingMode: "PAY_PER_REQUEST",
TableName: this.table
}),
this._createTableIfNotExists({
AttributeDefinitions: [
{
AttributeName: "installationId",
AttributeType: "S"
}
],
KeySchema: [
{
AttributeName: "installationId",
KeyType: "HASH"
}
],
BillingMode: "PAY_PER_REQUEST",
TableName: this.InstallationClientKeyTable
}),
this._createTableIfNotExists({
AttributeDefinitions: [
{
AttributeName: "installationId",
AttributeType: "S"
},
{
AttributeName: "key",
AttributeType: "S"
}
],
KeySchema: [
{
AttributeName: "installationId",
KeyType: "HASH"
},
{
AttributeName: "key",
KeyType: "RANGE"
}
],
BillingMode: "PAY_PER_REQUEST",
TableName: this.ForgeSettingsTable
})
]);
}
async _createTableIfNotExists(createTableInput) {
const { TableName } = createTableInput;
try {
await this.client.send(
new DescribeTableCommand({
TableName
})
);
} catch (error) {
if (error instanceof ResourceNotFoundException) {
this.logger.info(
`DynamoDB table not found: ${TableName}, attempting to create...`
);
try {
await this.client.send(new CreateTableCommand(createTableInput));
} catch (error) {
this.logger.error(`Failed to create table: ${TableName}`, error);
this.logger.info(
"Please see README for instructions on how to create the table manually."
);
}
} else {
throw error;
}
}
}
async get(key, clientKey) {
const res = await this.client.send(
new GetItemCommand({
TableName: this.table,
Key: {
clientKey: { S: clientKey },
key: { S: key }
},
ConsistentRead: true
})
);
return getAsObject(res?.Item?.val?.S);
}
async saveInstallation(val, clientKey) {
const clientSetting = await this.set("clientInfo", val, clientKey);
const forgeInstallationId = clientSetting.installationId;
if (forgeInstallationId) {
await this.associateInstallations(forgeInstallationId, clientKey);
}
return clientSetting;
}
async set(key, val, clientKey) {
await this.client.send(
new PutItemCommand({
TableName: this.table,
Item: {
clientKey: { S: clientKey },
key: { S: key },
val: { S: getAsString(val) },
createdAt: { N: new Date().getTime().toString() }
}
})
);
return this.get(key, clientKey);
}
async del(key, clientKey) {
await this.client.send(
new DeleteItemCommand({
TableName: this.table,
Key: {
clientKey: { S: clientKey },
key: { S: key }
}
})
);
}
async getAllClientInfos() {
let res = {};
let items = [];
let params = {
TableName: this.table,
FilterExpression: "#key = :key",
ExpressionAttributeNames: {
"#key": "key"
},
ExpressionAttributeValues: {
":key": { S: "clientInfo" }
}
};
do {
res = await this.client.send(new ScanCommand(params));
items = [].concat(items, res.Items);
params = Object.assign({}, params);
params.ExclusiveStartKey = res.LastEvaluatedKey;
} while (typeof res.LastEvaluatedKey !== "undefined");
return items
.sort(
(a, b) =>
(a.createdAt && parseInt(a.createdAt.N, 10)) -
(b.createdAt && parseInt(b.createdAt.N, 10))
)
.map(item => {
const val = item.val && item.val.S;
try {
return JSON.parse(val);
} catch (e) {
return val;
}
});
}
isMemoryStore() {
return false;
}
async associateInstallations(forgeInstallationId, clientKey) {
await this.client.send(
new UpdateItemCommand({
TableName: this.InstallationClientKeyTable,
Key: {
installationId: { S: forgeInstallationId }
},
UpdateExpression: "SET clientKey = :clientKey",
ExpressionAttributeValues: {
":clientKey": { S: clientKey }
}
})
);
}
async deleteAssociation(forgeInstallationId) {
try {
await this.client.send(
new DeleteItemCommand({
TableName: this.InstallationClientKeyTable,
Key: {
installationId: { S: forgeInstallationId }
}
})
);
return true;
} catch (error) {
if (error instanceof ResourceNotFoundException) {
return false;
}
throw error;
}
}
async getClientSettingsForForgeInstallation(forgeInstallationId) {
const keyResponse = await this.client.send(
new GetItemCommand({
TableName: this.InstallationClientKeyTable,
Key: {
installationId: { S: forgeInstallationId }
},
ConsistentRead: true
})
);
const clientKey =
keyResponse &&
keyResponse.Item &&
keyResponse.Item.clientKey &&
keyResponse.Item.clientKey.S;
if (!clientKey) {
return null;
}
const val = await this.get("clientInfo", clientKey);
if (!val) {
return null;
}
return val;
}
// Storage interface for Forge settings
forForgeInstallation(installationId) {
return {
del: async key => {
await this.client.send(
new DeleteItemCommand({
TableName: this.ForgeSettingsTable,
Key: {
installationId: { S: installationId },
key: { S: key }
}
})
);
},
get: async key => {
const res = await this.client.send(
new GetItemCommand({
TableName: this.ForgeSettingsTable,
Key: {
installationId: { S: installationId },
key: { S: key }
},
ConsistentRead: true
})
);
return getAsObject(res?.Item?.val?.S || null);
},
set: async (key, val) => {
await this.client.send(
new PutItemCommand({
TableName: this.ForgeSettingsTable,
Item: {
installationId: { S: installationId },
key: { S: key },
val: { S: getAsString(val) },
createdAt: { N: new Date().getTime().toString() }
}
})
);
return val;
}
};
}
}
module.exports = function (logger, opts) {
if (arguments.length === 0) {
return DynamoDBAdapter;
}
return new DynamoDBAdapter(logger, opts);
};