Source: dynamodb-table.js

const assert = require('assert');

const { AWSComponent } = require('./aws-component');
const { GenericComponent } = require('./generic-component');
const { Policy } = require('./policy');
const { Role } = require('./role');

const _ = require('lodash')

/* all:

dynamodb:BatchGetItem
dynamodb:BatchWriteItem
dynamodb:CreateTable
dynamodb:DeleteItem
dynamodb:DeleteTable
dynamodb:DescribeLimits
dynamodb:DescribeReservedCapacity
dynamodb:DescribeReservedCapacityOfferings
dynamodb:DescribeStream
dynamodb:DescribeTable
dynamodb:GetItem
dynamodb:GetRecords
dynamodb:GetShardIterator
dynamodb:ListStreams
dynamodb:ListTables
dynamodb:ListTagsOfResource
dynamodb:PurchaseReservedCapacityOfferings
dynamodb:PutItem
dynamodb:Query
dynamodb:Scan
dynamodb:TagResource
dynamodb:UpdateItem
dynamodb:UpdateTable
dynamodb:UntagResource

*/

/**
 * This class represents a DynamoDB table. 
 * @inheritdoc
 */
class DynamoDbTable extends GenericComponent {

  /**
   * Don't call this manually. Use creator function in {@link CloudFormation}
   * @param {string} stackName 
   * @param {string} baseName 
   */
  constructor(
    stackName,
    baseName) {
    super(stackName, baseName)
    
    this.type = "AWS::DynamoDB::Table";
    
    this._autoScalingRole = null;
    this._scalingItems = null;

    this.autoScaleInSecs = 30;
    this.autoScaleOutSecs = 30;

    this._scalingMinCapacity = 0;
    this._scalingMaxCapacity = 0;

  }

  /**
   * Get the table name.
   * THe table name is global in the same AWS account.
   */
  get tableName() {
    return `${this.stackName}_${this.baseName}`;
  }

  _updateAutoScalingItems() {

    // For detailed CloudFormation document: 
    // https://aws.amazon.com/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/

    if (this._scalingMaxCapacity <= 0 || this._scalingMinCapacity <= 0) {
      this._autoScalingRole = null;
      this._scalingItems = null;
    }
    else {
      this._autoScalingRole = new Role(this.stackName, this.baseName + 'AutoScaling');
      this._autoScalingRole.policyStatements = [{
        "Effect": "Allow",
        "Action": [
          "dynamodb:DescribeTable",
          "dynamodb:UpdateTable",
          "cloudwatch:PutMetricAlarm",
          "cloudwatch:DescribeAlarms",
          "cloudwatch:GetMetricStatistics",
          "cloudwatch:SetAlarmState",
          "cloudwatch:DeleteAlarms"
        ],
        "Resource": [
          this.getValue('ARN'),
          {
            "Fn::Join": ["", [this.getValue('ARN'), "/index/*"]]
          } //Also allow all indexes 
        ]
      }]

      this._autoScalingRole.assumeRolePolicyDocument = {
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Principal": {
            "Service": [
              "application-autoscaling.amazonaws.com"
            ]
          },
          "Action": [
            "sts:AssumeRole"
          ]
        }]
      }

      //Setup ScalableTarget object for this table and all GSI
      this._scalingItems = {};
      //For the table itself
      const readAndWrite = ['Read', 'Write'];
      for (let rw of readAndWrite) {
        const tableScalableTarget = `${this.fullName}${rw}ScalableTarget`;
        const autoScaleRoleARN = {
          "Fn::GetAtt": [
            this._autoScalingRole.fullName,
            "Arn"
          ]
        };
        this._scalingItems[tableScalableTarget] = {
          "Type": "AWS::ApplicationAutoScaling::ScalableTarget",
          "Properties": {
            "MaxCapacity": this._scalingMaxCapacity,
            "MinCapacity": this._scalingMinCapacity,
            "ResourceId": `table/${this.tableName}`,
            "RoleARN": autoScaleRoleARN,
            "ScalableDimension": `dynamodb:table:${rw}CapacityUnits`,
            "ServiceNamespace": "dynamodb"
          }
        }

        const tableScalingPolicy = `${this.fullName}${rw}ScalingPolicy`;

        this._scalingItems[tableScalingPolicy] = {
          "Type": "AWS::ApplicationAutoScaling::ScalingPolicy",
          "Properties": {
            "PolicyName": tableScalingPolicy,
            "PolicyType": "TargetTrackingScaling",
            "ScalingTargetId": {
              "Ref": tableScalableTarget
            },
            "TargetTrackingScalingPolicyConfiguration": {
              "TargetValue": 70,
              "ScaleInCooldown": this.autoScaleInSecs,
              "ScaleOutCooldown": this.autoScaleOutSecs,
              "PredefinedMetricSpecification": {
                "PredefinedMetricType": `DynamoDB${rw}CapacityUtilization`
              }
            }
          }
        }

        if (this.properties['GlobalSecondaryIndexes']) {
          for (let gsi of this.properties['GlobalSecondaryIndexes']) {
            const indexName = gsi['IndexName'];
            const indexScalableTarget = `${indexName}${rw}ScalableTarget`;
            this._scalingItems[indexScalableTarget] = {
              "Type": "AWS::ApplicationAutoScaling::ScalableTarget",
              "Properties": {
                "MaxCapacity": this._scalingMaxCapacity,
                "MinCapacity": this._scalingMinCapacity,
                "ResourceId": `table/${this.tableName}/index/${indexName}`,
                "RoleARN": autoScaleRoleARN,
                "ScalableDimension": `dynamodb:index:${rw}CapacityUnits`,
                "ServiceNamespace": "dynamodb"
              }
            }


            const indexScalingPolicy = `${indexName}${rw}ScalingPolicy`;
            this._scalingItems[indexScalingPolicy] = {
              "Type": "AWS::ApplicationAutoScaling::ScalingPolicy",
              "Properties": {
                "PolicyName": indexScalingPolicy,
                "PolicyType": "TargetTrackingScaling",
                "ScalingTargetId": {
                  "Ref": indexScalableTarget
                },
                "TargetTrackingScalingPolicyConfiguration": {
                  "TargetValue": 70,
                  "ScaleInCooldown": this.autoScaleInSecs,
                  "ScaleOutCooldown": this.autoScaleOutSecs,
                  "PredefinedMetricSpecification": {
                    "PredefinedMetricType": `DynamoDB${rw}CapacityUtilization`
                  }
                }
              }
            }
            
          } // for gsi of
        } //if
      }
    }
  }

  /**
   * Set the auto scaling for the table and its index. 
   * If maxCapacity <= 0, it means switching the auto scaling off. 
   * @param {int} minCapacity minimum throughput capacity
   * @param {int} maxCapacity maximum throughput capacity
   * @param {string} target optional, to set which one (table or GSI) the auto scaling applies. Notice: does not support this parameter for now.
   */
  setAutoScaling(minCapacity, maxCapacity, target) {

    // For detailed CloudFormation document: 
    // https://aws.amazon.com/blogs/database/how-to-use-aws-cloudformation-to-configure-auto-scaling-for-amazon-dynamodb-tables-and-indexes/
    assert.ok(!target, `Specified auto scaling target '${target}' which is not supported at the moment. auto scaling capacity will apply on the table and all GSIs`);
    assert.ok(minCapacity <= maxCapacity);
    assert.ok(minCapacity > 0);
    assert.ok(maxCapacity > 0);

    this._scalingMinCapacity = minCapacity;
    this._scalingMaxCapacity = maxCapacity;
  }

  /**
   * Use the properties attribute instead. 
   * @deprecated
   */
  setProperties(properties) {
    console.warn('setProperties is deprecated. Use ".properties = " instead ');
    let copied = _.clone(properties)
    copied['TableName'] = this.tableName


    //Default throughput values
    copied['ProvisionedThroughput'] = copied['ProvisionedThroughput'] || {
      ReadCapacityUnits: 2,
      WriteCapacityUnits: 2
    }

    this.properties = copied
  }

  policyStatementForAccessImpl(accessLevels, item) {
    //Item is ignored, because there is only one thing to offer 

    let actionsTable = {}
    actionsTable[AWSComponent.ACCESS_LEVEL_READ] = [
      'dynamodb:BatchGetItem',
      'dynamodb:DescribeLimits',
      'dynamodb:DescribeReservedCapacity',
      'dynamodb:DescribeReservedCapacityOfferings',
      "dynamodb:DescribeStream",
      "dynamodb:DescribeTable",
      "dynamodb:GetItem",
      "dynamodb:GetRecords",
      "dynamodb:GetShardIterator",
      "dynamodb:ListStreams",
      "dynamodb:ListTables",
      "dynamodb:ListTagsOfResource",
      "dynamodb:Query",
      "dynamodb:Scan",
    ]

    actionsTable[AWSComponent.ACCESS_LEVEL_WRITE] = [
      'dynamodb:BatchWriteItem',
      'dynamodb:DeleteItem',
      "dynamodb:PutItem",
      "dynamodb:TagResource",
      "dynamodb:UpdateItem",
      "dynamodb:UpdateTable",
      "dynamodb:UntagResource"
    ]

    actionsTable[AWSComponent.ACCESS_LEVEL_ADMIN] = [
      'dynamodb:CreateTable',
      'dynamodb:DeleteTable',
      "dynamodb:PurchaseReservedCapacityOfferings",
    ]

    let allowedActions = []
    accessLevels.forEach((level) => {
      let actions = actionsTable[level]
      allowedActions = allowedActions.concat(actions)
    })

    return {
      "Effect": "Allow",
      "Action": allowedActions.sort(),
      "Resource": [
        this.getValue('ARN'),
        {
          "Fn::Join": ["", [this.getValue('ARN'), "/index/*"]]
        } //Also allow all indexes 
      ]
    }
  }

  get template() {
    let template = {};
    
    const appliedProperties = _.clone(this.properties);
    appliedProperties['TableName'] = this.tableName;
    
    //Default throughput values
    appliedProperties['ProvisionedThroughput'] = appliedProperties['ProvisionedThroughput'] || {
      ReadCapacityUnits: 1,
      WriteCapacityUnits: 1
    }
    
    template[this.fullName] = {
      "Type": "AWS::DynamoDB::Table",
      "Properties": appliedProperties
    }

    this._updateAutoScalingItems();

    if (this._autoScalingRole && this._scalingItems) {
      template = _.merge(template, this._scalingItems);
      template = _.merge(template, this._autoScalingRole.template);
    }
    return template
  }
}

exports.DynamoDbTable = DynamoDbTable