Dynamoid/dynamoid

View on GitHub
lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

require_relative 'until_past_table_status'

module Dynamoid
  # @private
  module AdapterPlugin
    class AwsSdkV3
      class CreateTable
        attr_reader :client, :table_name, :key, :options

        def initialize(client, table_name, key, options)
          @client = client
          @table_name = table_name
          @key = key
          @options = options
        end

        def call
          billing_mode = options[:billing_mode]
          read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
          write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity

          secondary_indexes = options.slice(
            :local_secondary_indexes,
            :global_secondary_indexes
          )
          ls_indexes = options[:local_secondary_indexes]
          gs_indexes = options[:global_secondary_indexes]

          key_schema = {
            hash_key_schema: { key => options[:hash_key_type] || :string },
            range_key_schema: options[:range_key]
          }
          attribute_definitions = build_all_attribute_definitions(
            key_schema,
            secondary_indexes
          )
          key_schema = aws_key_schema(
            key_schema[:hash_key_schema],
            key_schema[:range_key_schema]
          )

          client_opts = {
            table_name: table_name,
            key_schema: key_schema,
            attribute_definitions: attribute_definitions
          }

          if billing_mode == :on_demand
            client_opts[:billing_mode] = 'PAY_PER_REQUEST'
          else
            client_opts[:billing_mode] = 'PROVISIONED'
            client_opts[:provisioned_throughput] = {
              read_capacity_units: read_capacity,
              write_capacity_units: write_capacity
            }
          end

          if ls_indexes.present?
            client_opts[:local_secondary_indexes] = ls_indexes.map do |index|
              index_to_aws_hash(index)
            end
          end

          if gs_indexes.present?
            client_opts[:global_secondary_indexes] = gs_indexes.map do |index|
              index_to_aws_hash(index)
            end
          end
          resp = client.create_table(client_opts)
          options[:sync] = true if (!options.key?(:sync) && ls_indexes.present?) || gs_indexes.present?

          if options[:sync]
            status = PARSE_TABLE_STATUS.call(resp, :table_description)
            if status == TABLE_STATUSES[:creating]
              UntilPastTableStatus.new(client, table_name, :creating).call
            end
          end

          # Response to original create_table, which, if options[:sync]
          # may have an outdated table_description.table_status of "CREATING"
          resp
        end

        private

        # Builds aws attributes definitions based off of primary hash/range and
        # secondary indexes
        #
        # @param key_schema
        # @option key_schema [Hash] hash_key_schema - eg: {:id => :string}
        # @option key_schema [Hash] range_key_schema - eg: {:created_at => :number}
        # @param [Hash] secondary_indexes
        # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :local_secondary_indexes
        # @option secondary_indexes [Array<Dynamoid::Indexes::Index>] :global_secondary_indexes
        def build_all_attribute_definitions(key_schema, secondary_indexes = {})
          ls_indexes = secondary_indexes[:local_secondary_indexes]
          gs_indexes = secondary_indexes[:global_secondary_indexes]

          attribute_definitions = []

          attribute_definitions << build_attribute_definitions(
            key_schema[:hash_key_schema],
            key_schema[:range_key_schema]
          )

          if ls_indexes.present?
            ls_indexes.map do |index|
              attribute_definitions << build_attribute_definitions(
                index.hash_key_schema,
                index.range_key_schema
              )
            end
          end

          if gs_indexes.present?
            gs_indexes.map do |index|
              attribute_definitions << build_attribute_definitions(
                index.hash_key_schema,
                index.range_key_schema
              )
            end
          end

          attribute_definitions.flatten!
          # uniq these definitions because range keys might be common between
          # primary and secondary indexes
          attribute_definitions.uniq!
          attribute_definitions
        end

        # Builds an attribute definitions based on hash key and range key
        # @param [Hash] hash_key_schema - eg: {:id => :string}
        # @param [Hash] range_key_schema - eg: {:created_at => :datetime}
        # @return [Array]
        def build_attribute_definitions(hash_key_schema, range_key_schema = nil)
          attrs = []

          attrs << attribute_definition_element(
            hash_key_schema.keys.first,
            hash_key_schema.values.first
          )

          if range_key_schema.present?
            attrs << attribute_definition_element(
              range_key_schema.keys.first,
              range_key_schema.values.first
            )
          end

          attrs
        end

        # Builds an aws attribute definition based on name and dynamoid type
        # @param [Symbol] name - eg: :id
        # @param [Symbol] dynamoid_type - eg: :string
        # @return [Hash]
        def attribute_definition_element(name, dynamoid_type)
          aws_type = api_type(dynamoid_type)

          {
            attribute_name: name.to_s,
            attribute_type: aws_type
          }
        end

        # Converts from symbol to the API string for the given data type
        # E.g. :number -> 'N'
        def api_type(type)
          case type
          when :string then STRING_TYPE
          when :number then NUM_TYPE
          when :binary then BINARY_TYPE
          else raise "Unknown type: #{type}"
          end
        end

        # Converts a Dynamoid::Indexes::Index to an AWS API-compatible hash.
        # This resulting hash is of the form:
        #
        #   {
        #     index_name: String
        #     keys: {
        #       hash_key: aws_key_schema (hash)
        #       range_key: aws_key_schema (hash)
        #     }
        #     projection: {
        #       projection_type: (ALL, KEYS_ONLY, INCLUDE) String
        #       non_key_attributes: (optional) Array
        #     }
        #     provisioned_throughput: {
        #       read_capacity_units: Integer
        #       write_capacity_units: Integer
        #     }
        #   }
        #
        # @param [Dynamoid::Indexes::Index] index the index.
        # @return [Hash] hash representing an AWS Index definition.
        def index_to_aws_hash(index)
          key_schema = aws_key_schema(index.hash_key_schema, index.range_key_schema)

          hash = {
            index_name: index.name,
            key_schema: key_schema,
            projection: {
              projection_type: index.projection_type.to_s.upcase
            }
          }

          # If the projection type is include, specify the non key attributes
          if index.projection_type == :include
            hash[:projection][:non_key_attributes] = index.projected_attributes
          end

          # Only global secondary indexes have a separate throughput.
          if index.type == :global_secondary && options[:billing_mode] != :on_demand
            hash[:provisioned_throughput] = {
              read_capacity_units: index.read_capacity,
              write_capacity_units: index.write_capacity
            }
          end
          hash
        end

        # Converts hash_key_schema and range_key_schema to aws_key_schema
        # @param [Hash] hash_key_schema eg: {:id => :string}
        # @param [Hash] range_key_schema eg: {:created_at => :number}
        # @return [Array]
        def aws_key_schema(hash_key_schema, range_key_schema)
          schema = [{
            attribute_name: hash_key_schema.keys.first.to_s,
            key_type: HASH_KEY
          }]

          if range_key_schema.present?
            schema << {
              attribute_name: range_key_schema.keys.first.to_s,
              key_type: RANGE_KEY
            }
          end
          schema
        end
      end
    end
  end
end