import * as AWS from 'aws-sdk'; import {response, IResponse} from './tools'; AWS.config.update({region: 'us-east-1'}); // Virginia const kms = new AWS.KMS({apiVersion: '2014-11-01'}); // const kmsKeyId = process.env.KMS_PLAYER_ARN_KEY; // let kmsKeyId; /** * * @param {string} data * @param {string} kmsKeyId * @returns {Promise} */ function encrypt(data: string, kmsKeyId: string): Promise { return new Promise((res, rej) => { try { if (!data || data === '') { // data is null/undefined so we are not going to encrypt it return res(null); } kms.encrypt({KeyId: kmsKeyId, Plaintext: data}, function (err, _data) { if (err) { rej(err); } else { res(_data); } }); } catch (error) { rej(error.message); } }); } /** * * @param blob * @param {string} kmsKeyId * @returns {Promise} */ function decrypt(blob: any, kmsKeyId: string): Promise { return new Promise((res, rej) => { try { if ((blob && !blob.CiphertextBlob) || !blob) { // blob is just a non-encrypted string return res(blob); } kms.decrypt({CiphertextBlob: blob.CiphertextBlob}, function (err, _data) { if (err) { console.log(err); rej(err); } else { res(_data); } }); } catch (error) { console.log(143, error.message); rej(error.message); } }); } export interface IDynamoParams { TableName: string; IndexName?: string; Key?: any; Item?: any; KeyConditionExpression?: string; ConditionExpression?: string; ExpressionAttributeValues?: any; ExpressionAttributeNames?: any; UpdateExpression?: string; FilterExpression?: string; Limit?: number; LastEvaluatedKey?: any; ExclusiveStartKey?: any; ScanIndexForward?: boolean; ProjectionExpression?: string; force?: string; // for delete purposes ScannedCount?: any; } export class DynamoDbApi { dynamoDb: any; kmsKeyId: string; constructor(dynamoDb: any, _kmsKeyId?: string) { this.dynamoDb = dynamoDb; this.kmsKeyId = _kmsKeyId; process.setMaxListeners(Infinity); } /** * * @param item * @param {Array} decryptFields * @returns {Promise} */ async decryptItem(item: any, decryptFields: Array): Promise { try { const Keys = Object.keys(item); for (let i = 0; i < Keys.length; i++) { // is it an encrypted data, and do we want to decrypt it if (item[Keys[i]] && item[Keys[i]]['CiphertextBlob'] && decryptFields.indexOf(Keys[i]) > -1) { const decryptedTmp = await decrypt(item[Keys[i]], this.kmsKeyId); if (decryptedTmp && decryptedTmp.Plaintext) { const val = decryptedTmp.Plaintext.toString(); item[Keys[i]] = val.indexOf('{') > -1 ? JSON.parse(val) : val; } } } } catch (error) { console.log('decryptItem():', error); } return item; } /** * * @param item * @param {Array} encryptFields * @returns {Promise} */ async encryptItem(item: any, encryptFields: Array): Promise { try { const Keys = Object.keys(item); for (let i = 0; i < Keys.length; i++) { // is it an encrypted data, and do we want to decrypt it if (item[Keys[i]] && encryptFields.indexOf(Keys[i]) > -1) { const encryptedTmp = await encrypt(typeof item[Keys[i]] === 'object' ? JSON.stringify(item[Keys[i]]) : item[Keys[i]], this.kmsKeyId); if (encryptedTmp) { item[Keys[i]] = encryptedTmp; } } } } catch (error) { console.log('tools.encryptItem() error:', error.message); } finally { return item; } } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} */ get(params: IDynamoParams, context: string, decryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; return new Promise((res, rej) => { if (!dynamoDb) { return rej(response(false, 'dynamoDbTable undefined')); } try { dynamoDb.get(params, async (error, result) => { if (error) { return rej(response(false, error, `Could not get '${context}' info`)); } else if (result.Item) { if (decryptFields && decryptFields.length) { result.Item = await this.decryptItem(result.Item, decryptFields); } return res(response(true, null, result.Item)); } else { return res(response(false, `'${context}' info not found`)); } }); } catch (error) { console.log(479, error.message); rej(response(false, error)); } }) } /** * * @param dynamoDb * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} * @private */ private async _scan(dynamoDb: any, params: IDynamoParams, context: string, decryptFields?: Array): Promise { if (!dynamoDb) { return response(false, 'dynamoDbTable undefined'); } try { let count = 0; const result = await dynamoDb.scan(params).promise(); if (result.Items && result.Items.length) { if (decryptFields && decryptFields.length) { for (let i = 0; i < result.Items.length; i++) { result.Items[i] = await this.decryptItem(result.Items[i], decryptFields); } } return response(true, null, result); } else { return response(false, `${context} info not found`, result); } } catch (error) { console.log('263 Error tools.db._scan()', error.message, params); return response(false, error); } } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} */ async scan(params: IDynamoParams, context: string, decryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; try { let items = [], result, whileMax = 1000, i = 0, count = 0, done = false, LastEvaluatedKey = null; while (!done && (i < whileMax)) { result = await this._scan(dynamoDb, params, context, decryptFields); if (!result.success && !params.Limit) { done = true; return result; } else { if (params.Limit) { if (result.data) { count += result.data.Count; if (!result.data.LastEvaluatedKey) { done = true; items.push(...result.data.Items); } else { if (count === params.Limit) { done = true; items.push(...result.data.Items); LastEvaluatedKey = result.data.LastEvaluatedKey; } else if (count > params.Limit) { done = true; const keys = Object.keys(result.data.LastEvaluatedKey); items.push(...result.data.Items.slice(0, (count - params.Limit))); for (let j = 0; j < keys.length; j++) { LastEvaluatedKey[keys[j]] = result.data.Items[count - params.Limit - 1][keys[j]]; } } else { LastEvaluatedKey = result.data.LastEvaluatedKey; params['ExclusiveStartKey'] = LastEvaluatedKey; items.push(...result.data.Items); // let's go for a new loop then } } } else { // let's go for a new loop then LastEvaluatedKey = result.data.LastEvaluatedKey; params['ExclusiveStartKey'] = LastEvaluatedKey; } } else { done = true; LastEvaluatedKey = result.data.LastEvaluatedKey; items.push(...result.data.Items); } } i++; } return response(true, null, { Items: items, Count: items.length, CountV2: result.data.ScannedCount, LastEvaluatedKey: LastEvaluatedKey, Loops: i }); } catch (error) { return response(false, error); } } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} */ async query(params: IDynamoParams, context: string, decryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; if (!dynamoDb) { return response(false, 'dynamoDbTable undefined'); } try { const result = await dynamoDb.query(params).promise(); if (result.Items && result.Items.length) { if (decryptFields && decryptFields.length) { for (let i = 0; i < result.Items.length; i++) { result.Items[i] = await this.decryptItem(result.Items[i], decryptFields); } } return response(true, null, result.Items.length > 1 ? result.Items : result.Items[0]); } else { return response(false, `'${context}' info not found`); } } catch (error) { console.log('263 Error tools.db.query()', error.message, params); return response(false, error.message, `Could not get '${context}' info`); } } /** * Return the raw result from dynamodb, instead only "Items" * * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} */ async queryRAW(params: IDynamoParams, context: string, decryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; if (!dynamoDb) { return response(false, 'dynamoDbTable undefined'); } try { let result = await dynamoDb.query(params).promise(); if(!result){ return response(false, `'${context}' - Error on dynamoDb.query(params).promise().`); } let promises = []; if (result.Items && result.Items.length) { if (decryptFields && decryptFields.length) { for (let i = 0; i < result.Items.length; i++) { promises.push(this.decryptItem(result.Items[i], decryptFields)) } result = await Promise.all(promises).then(results => { delete result.Items; result['Items'] = results; return result; }); } return response(true, null, result); } else { return response(false, `'${context}' info not found`, result); } } catch (error) { console.log('263 Error tools.db.query()', error.message, params); return response(false, error.message, `Could not get '${context}' info`); } } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} decryptFields * @returns {Promise} */ delete(params: IDynamoParams, context: string, decryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; return new Promise((res, rej) => { if (!dynamoDb) { return rej(response(false, 'dynamoDbTable undefined')); } try { if (params.force) { dynamoDb.delete(params, async (error, result) => { if (error) { return rej(response(false, error, `Could not delete '${context}' info`)); } return res(response(true, null, result)); }); } else { if (!params.Key) { return response(false, 'Key is required. Cannot delete item'); } // building a clean query const _params = { TableName: params.TableName, Key: params.Key, UpdateExpression: "set deleted =:d", ExpressionAttributeValues: { ":d": true } }; dynamoDb.update(_params, (error, result) => { if (error) { console.log(params, error); return rej(response(false, error, `Could not update '${context}' item to 'deleted' state`)); } else { return res(response(true, null, result)); } }); } } catch (error) { console.log(479, error.message); rej(response(false, error)); } }) } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} encryptFields * @returns {Promise} */ put(params: IDynamoParams, context: string, encryptFields?: Array): Promise { return new Promise(async (res, rej) => { const dynamoDb = this.dynamoDb; try { if (!dynamoDb) { return rej(response(false, 'dynamoDbTable undefined')); } if (encryptFields && encryptFields.length) { params.Item = await this.encryptItem(params.Item, encryptFields); } //Adding created and updated params.Item.updatedAt = new Date().toISOString(); params.Item.createdAt = new Date().toISOString(); dynamoDb.put(params, (error, result) => { if (error) { console.log(params, error); return rej(response(false, error, `Could not insert '${context}' item`)); } else { return res(response(true, null, result)); } }); } catch (error) { console.log('295 tools.db.put()', error); rej(response(false, error.message)); } }); } /** * * @param item * @param {Array} encryptFields * @returns {Promise<{expressions: any; updates: any}>} */ async getUpdateExpression(item: any, encryptFields?: Array): Promise<{ expressions: any, updates: any }> { let expressions = {}, updates = ''; if (encryptFields) { item = await this.encryptItem(item, encryptFields); } const Keys = Object.keys(item); for (let i = 0; i < Keys.length; i++) { // if (Keys[i] === 'id') expressions[`:v${i}`] = item[Keys[i]]; updates += `${Keys[i]} =:v${i},`; } updates = updates.replace(/(^,)|(,$)/g, ""); return { expressions: expressions, updates: updates }; } /** * * * * @param item * @param {Array} encryptFields * @returns {Promise<{expressions: any; updates: any}>} */ async getUpdateExpressionV2(item: any, encryptFields?: Array): Promise<{ attributesValue: any, updates: any, attributesName: any }> { let attributesValue = {}, attributesName = {}, updates = ''; if (encryptFields) { item = await this.encryptItem(item, encryptFields); } const Keys = Object.keys(item); for (let i = 0; i < Keys.length; i++) { // if (Keys[i] === 'id') attributesValue[`:v${i}`] = item[Keys[i]]; attributesName[`#a${i}`] = Keys[i]; updates += `#a${i} =:v${i},`; } updates = updates.replace(/(^,)|(,$)/g, ""); return { attributesValue: attributesValue, updates: updates, attributesName: attributesName }; } /** * * @param {IDynamoParams} params * @param {string} context * @param {Array} encryptFields * @returns {Promise} */ update(params: IDynamoParams, context: string, encryptFields?: Array): Promise { const dynamoDb = this.dynamoDb; return new Promise((res, rej) => { if (!dynamoDb) { return rej(response(false, 'dynamoDbTable undefined')); } try { // Adding updatedAt get the time at insert. params.ExpressionAttributeValues[':upd'] = new Date().toISOString(); params.UpdateExpression += ', updatedAt =:upd'; dynamoDb.update(params, (error, result) => { if (error) { console.log(params, error); return rej(response(false, error)); } else { return res(response(true, null, result)); } }); } catch (error) { console.log(error) rej(response(false, error)); } }); } }