UNPKG

atlassian-connect-express

Version:

Library for building Atlassian Add-ons on top of Express

336 lines (309 loc) 8.19 kB
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); };