class Aws::Dynamodb::Query::Query

Public Class Methods

call(table_name, model_class: nil, nullify_model_attributes: false, result_type: Struct, index_name: nil, select: nil, key_condition_expression: nil, expression_attribute_names: nil, expression_attribute_values: nil, scan_index_forward: true, return_consumed_capacity: nil, pagination: false, last_evaluated_key: nil, aws_region: ENV['AWS_REGION'], aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'], aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], aws_dynamodb_endpoint: ENV['aws_dynamodb_endpoint']) click to toggle source

Queries Dynamodb for items. @see docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html

@param [String] table_name The name of the table containing the requested items. @param [Class] model_class model record class used to find stored attributes @param [Boolean] nullify_model_attributes false by default, when true assigns null to model attributes that are not returned with DB resultset @param [Class] result_type model_class (pass the record class e.g. User), Struct, or Hash @param [String] index_name The name of an index to query @param [String] select The attributes to be returned in the result: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, COUNT, SPECIFIC_ATTRIBUTES @param [String] key_condition_expression The condition that specifies the key value(s) for items to be retrieved by the Query action @param [Hash] expression_attribute_names One or more substitution tokens for attribute names in an expression @param [Hash] expression_attribute_values One or more values that can be substituted in an expression @param [String] scan_index_forward Specifies the order for index traversal: If true (default), the traversal is performed in ascending order; if false, the traversal is performed in descending order @param [String] return_consumed_capacity Determines the level of detail about provisioned throughput consumption that is returned in the response: INDEXES, TOTAL, NONE @param [Boolean] pagination true by default. This will get maximum 1MB of data. To retrieve all your table data which could exceed 1MB, set pagination to false. @param [String] last_evaluated_key if pagination is true, this will point to the next page of data to retrieve @param [String] aws_region AWS region is obtained by default from ENV, use this parameter to override with your own @param [String] aws_access_key_id AWS access key id is obtained by default from ENV, use this parameter to override with your own @param [String] aws_secret_access_key AWS secret access key is obtained by default from ENV, use this parameter to override with your own @param [String] aws_dynamodb_endpoint AWS dynamodb endpoint is obtained by default from ENV, use this parameter to override with your own

@return [Array] array of Ruby Hash objects where each object contains all the values returned from dynamodb in dasherized format

# File lib/aws/dynamodb/query.rb, line 34
def self.call(table_name,
              model_class: nil,
              nullify_model_attributes: false,
              result_type: Struct,
              index_name: nil,
              select: nil,
              key_condition_expression: nil,
              expression_attribute_names: nil,
              expression_attribute_values: nil,
              scan_index_forward: true,
              return_consumed_capacity: nil,
              pagination: false,
              last_evaluated_key: nil,
              aws_region: ENV['AWS_REGION'],
              aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
              aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
              aws_dynamodb_endpoint: ENV['aws_dynamodb_endpoint'])
  result = []
  # DyanmoDB Query results are divided into "pages" of data that are 1 MB in size (or less).
  # Agents data exceeds 1MB so we need multiple pages.
  # @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Pagination
  last_evaluated_key = last_evaluated_key
  loop do
    payload = {
      TableName: table_name
    }
    # optional query parameters
    payload[:IndexName]                 = index_name if index_name
    payload[:select]                    = select if select
    payload[:KeyConditionExpression]    = key_condition_expression if key_condition_expression
    payload[:ExpressionAttributeNames]  = expression_attribute_names if expression_attribute_names
    payload[:ExpressionAttributeValues] = expression_attribute_values if expression_attribute_values
    payload[:scan_index_forward]        = scan_index_forward if scan_index_forward
    payload[:ReturnConsumedCapacity]    = return_consumed_capacity if return_consumed_capacity
    payload[:ExclusiveStartKey]         = last_evaluated_key if last_evaluated_key

    # generate AWS Authorization header
    aws_signature = Aws::Signature::V4::Signature.new(aws_region, aws_access_key_id, aws_secret_access_key)
    aws_signature.generate_signature('dynamodb', 'DynamoDB_20120810.Query', 'POST', payload, aws_dynamodb_endpoint, '/')
    # Create the HTTP objects
    uri = URI.parse(aws_dynamodb_endpoint)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = aws_dynamodb_endpoint.include?('https')
    request = Net::HTTP::Post.new(uri.request_uri, aws_signature.headers)
    request.body = payload.to_json
    res = http.request(request)

    # error => return empty array
    unless res.is_a? Net::HTTPSuccess
      Rails.logger.error "Error querying DynamoDB: code=#{res.code} message=#{res.message} body=#{res.body}" if defined? Rails

      # jsonapi error format
      result = {
        errors: [
          status: res.code,
          title:  res.message,
          detail: res.body
        ]
      }
      return result
    end

    # Create DAO struct object <struct Struct::Customer id>
    Struct.new('AwsDynamodbQueryDAO', *model_class.attributes.attributes.keys) if result_type.eql? Struct

    # success => parse returned json
    json = Yajl::Parser.new.parse(res.body)

    # extract and construct resultset array
    json['Items'].each do |item|
      record = {}
      stored_attributes = []
      item.keys.each do |key|
        # @see: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html
        # Sample dynamodb item: {"date_joined"=>{"S"=>"2017-09-30T16:00:00+00:00"}, "contact_country_code"=>{"NULL"=>true}, "full_name"=>{"S"=>"Breena"}, "has_bank_swift"=>{"BOOL"=>false}}
        # NULL will fall back to default nil
        # Number will be handled afterwards
        dynamodb_data_types = %w[S BS L M NS S SS BOOL]
        # value could be found in one of these data types
        key_dasherized = key.to_s.dasherize
        dynamodb_data_types.each do |data_type|
          record[key_dasherized] = record[key_dasherized] || item[key][data_type]
        end

        # handle numbers: integer or float
        value = item[key]['N']
        if record[key_dasherized].nil? and value.present?
          begin
            record[key_dasherized] = Integer(value)
          rescue ArgumentError
            record[key_dasherized] = Float(value)
          end
        end

        stored_attributes += [key.to_sym]
      end

      # nullify remaining model attributes to be included in returned result
      if nullify_model_attributes and model_class.respond_to? :attributes
        unset_attributes = model_class.attributes.attributes.keys - stored_attributes
        unset_attributes.each do |attr|
          record[attr.to_s.dasherize] = nil
        end
      end

      case result_type.to_s
        when Struct.to_s
          # order values in the same order as model attribute_keys
          attribute_values = []
          model_class.attributes.attributes.keys.each do |attribute_key|
            attribute_values << record[attribute_key.to_s.dasherize]
          end
          result << Struct::AwsDynamodbQueryDAO.new(*attribute_values)
        when model_class.to_s
          result << model_class.new(record)
        else
          result << record
      end
    end

    # LastEvaluatedKey in the response indicates that not all of the items have been retrieved.
    # It should be used as the ExclusiveStartKey for the next Query request to retrieve the next page items.
    last_evaluated_key = json['LastEvaluatedKey']
    # Break if pagination is enabled. Else, absence of LastEvaluatedKey indicates that there are no more pages to retrieve.
    break if pagination.present? || last_evaluated_key.blank?
  end
  result
end