├── .rubocop_todo.yml ├── .rspec ├── lib └── dynamodb │ ├── api │ ├── version.rb │ ├── relation │ │ ├── limit_clause.rb │ │ ├── select_clause.rb │ │ ├── from_clause.rb │ │ ├── global_secondary_index.rb │ │ ├── order_clause.rb │ │ ├── filter_clause.rb │ │ ├── query_methods.rb │ │ ├── expression_attribute_names.rb │ │ └── where_clause.rb │ ├── delete │ │ ├── tables.rb │ │ └── item.rb │ ├── put │ │ └── item.rb │ ├── adapter.rb │ ├── map │ │ └── operator.rb │ ├── config.rb │ ├── relation.rb │ ├── config │ │ └── options.rb │ ├── update │ │ ├── attributes.rb │ │ ├── base.rb │ │ └── item.rb │ ├── scan.rb │ ├── query.rb │ └── base.rb │ └── api.rb ├── .rubocop.yml ├── Rakefile ├── bin ├── setup └── console ├── Gemfile ├── gemfiles ├── aws_sdk_2_rails_5.gemfile ├── aws_sdk_2_rails_6.gemfile ├── aws_sdk_3_rails_5.gemfile └── aws_sdk_3_rails_6.gemfile ├── spec ├── dynamodb │ ├── api │ │ ├── adapter_spec.rb │ │ ├── relation │ │ │ ├── limit_clause_spec.rb │ │ │ ├── from_clause_spec.rb │ │ │ ├── filter_clause_spec.rb │ │ │ ├── expression_attribute_names_spec.rb │ │ │ └── where_clause_spec.rb │ │ ├── put │ │ │ └── item_spec.rb │ │ ├── delete │ │ │ └── item_spec.rb │ │ ├── update │ │ │ ├── attributes_spec.rb │ │ │ └── item_spec.rb │ │ ├── scan_spec.rb │ │ └── query_spec.rb │ └── api_spec.rb ├── spec_helper.rb └── support │ └── dynamodb_helper.rb ├── docker-compose.yml ├── .pryrc ├── .gitignore ├── Dockerfile ├── Appraisals ├── .github └── workflows │ └── build.yml ├── LICENSE.txt ├── dynamodb-api.gemspec ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | Airbnb/OptArgParameters: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/dynamodb/api/version.rb: -------------------------------------------------------------------------------- 1 | module Dynamodb 2 | module Api 3 | VERSION = '0.9.1'.freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | require: 4 | - rubocop-airbnb 5 | 6 | inherit_from: 7 | - .rubocop_todo.yml 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in dynamodb-api.gemspec 6 | gemspec 7 | 8 | gem 'pry-byebug', platforms: :ruby 9 | -------------------------------------------------------------------------------- /gemfiles/aws_sdk_2_rails_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry-byebug", platforms: :ruby 6 | gem "aws-sdk", "~> 2" 7 | gem "activesupport", "~> 5" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/aws_sdk_2_rails_6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry-byebug", platforms: :ruby 6 | gem "aws-sdk", "~> 2" 7 | gem "activesupport", "~> 6" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/aws_sdk_3_rails_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry-byebug", platforms: :ruby 6 | gem "aws-sdk", "~> 3" 7 | gem "activesupport", "~> 5" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/aws_sdk_3_rails_6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "pry-byebug", platforms: :ruby 6 | gem "aws-sdk", "~> 3" 7 | gem "activesupport", "~> 6" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /spec/dynamodb/api/adapter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dynamodb::Api::Adapter do 2 | describe '#initialize' do 3 | it 'has a aws dynamodb client' do 4 | expect(Dynamodb::Api::Adapter.client.class).to be Aws::DynamoDB::Client 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ruby: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | volumes: 8 | - .:/app 9 | dynamodb: 10 | image: amazon/dynamodb-local 11 | ports: 12 | - 8000:8000 13 | -------------------------------------------------------------------------------- /.pryrc: -------------------------------------------------------------------------------- 1 | if defined?(PryByebug) 2 | Pry.commands.alias_command '_c', 'continue' 3 | Pry.commands.alias_command '_s', 'step' 4 | Pry.commands.alias_command '_n', 'next' 5 | Pry.commands.alias_command '_f', 'finish' 6 | Pry.commands.alias_command '_w', 'whereami' 7 | end 8 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/limit_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class LimitClause # :nodoc: 7 | attr_reader :number 8 | 9 | def initialize(number) 10 | @number = number 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dynamodb/api/relation/limit_clause_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Relation::LimitClause do 4 | describe '#initialize' do 5 | it 'returns limit number' do 6 | expect( 7 | Dynamodb::Api::Relation::LimitClause.new(1).number 8 | ).to eq(1) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/select_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class SelectClause # :nodoc: 7 | attr_reader :name 8 | 9 | def initialize(name = nil) 10 | @name = name || 'ALL_ATTRIBUTES' 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /vendor/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | 14 | .DS_Store 15 | .ruby-version 16 | Gemfile.lock 17 | 18 | # For Ctags 19 | tags 20 | 21 | # For Appraisals 22 | gemfiles/*.gemfile.lock 23 | gemfiles/.bundle/* 24 | gemfiles/vendor/* 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.5.5 2 | 3 | ENV APP_ROOT /app 4 | WORKDIR $APP_ROOT 5 | 6 | COPY . $APP_ROOT 7 | 8 | RUN \ 9 | echo 'gem: --no-document' >> ~/.gemrc && \ 10 | cp ~/.gemrc /etc/gemrc && \ 11 | chmod uog+r /etc/gemrc && \ 12 | bundle config --global build.nokogiri --use-system-libraries && \ 13 | bundle config --global jobs 4 && \ 14 | bin/setup && \ 15 | rm -rf ~/.gem 16 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/from_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class FromClause # :nodoc: 7 | attr_reader :name 8 | 9 | # @param name [String] the table name 10 | def initialize(name) 11 | @name = Dynamodb::Api::Config.build_table_name(name) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "aws-sdk-2-rails-5" do 2 | gem "aws-sdk", "~> 2" 3 | gem "activesupport", "~> 5" 4 | end 5 | 6 | appraise "aws-sdk-2-rails-6" do 7 | gem "aws-sdk", "~> 2" 8 | gem "activesupport", "~> 6" 9 | end 10 | 11 | appraise "aws-sdk-3-rails-5" do 12 | gem "aws-sdk", "~> 3" 13 | gem "activesupport", "~> 5" 14 | end 15 | 16 | appraise "aws-sdk-3-rails-6" do 17 | gem "aws-sdk", "~> 3" 18 | gem "activesupport", "~> 6" 19 | end 20 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/global_secondary_index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class GlobalSecondaryIndex 7 | attr_reader :name 8 | 9 | def initialize(name) 10 | if Dynamodb::Api::Config.index_name_prefix? 11 | name = Dynamodb::Api::Config.index_name_prefix + name 12 | end 13 | @name = name 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dynamodb/api/delete/tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Delete 6 | class Tables # :nodoc: 7 | # TODO: add rspec 8 | def self.delete_tables 9 | client = Adapter.client 10 | table_names = client.list_tables[:table_names] 11 | table_names.each do |table_name| 12 | client.delete_table(table_name: table_name) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dynamodb/api/put/item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Put 6 | class Item 7 | # @param key [Hash] the item key 8 | # @param table_name [String] the table name 9 | def self.put_item(item, table_name) 10 | client = Adapter.client 11 | table_name = Dynamodb::Api::Config.build_table_name(table_name) 12 | 13 | client.put_item(item: item, table_name: table_name) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dynamodb/api/delete/item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Delete 6 | class Item 7 | # @param key [Hash] the item key 8 | # @param table_name [String] the table name 9 | def self.delete_item(key, table_name) 10 | client = Adapter.client 11 | table_name = Dynamodb::Api::Config.build_table_name(table_name) 12 | 13 | client.delete_item(key: key, table_name: table_name) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/order_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class OrderClause # :nodoc: 7 | attr_reader :direct 8 | 9 | def initialize(direct = nil) 10 | @direct = key_map(direct) 11 | end 12 | 13 | private 14 | 15 | def key_map(direct) 16 | case direct 17 | when nil 18 | false 19 | when 'desc' 20 | false 21 | when 'asc' 22 | true 23 | else 24 | false 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/filter_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class FilterClause # :nodoc: 7 | attr_reader :expression 8 | attr_reader :values 9 | attr_reader :reserved_words 10 | 11 | def initialize(expression, values) 12 | @expression = expression 13 | @values = values 14 | @reserved_words = extract_reserved_words(@expression) 15 | end 16 | 17 | private 18 | 19 | def extract_reserved_words(expression) 20 | expression.scan(/\#\w+/) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dynamodb/api/adapter.rb: -------------------------------------------------------------------------------- 1 | module Dynamodb 2 | module Api 3 | class Adapter # :nodoc: 4 | attr_reader :_client 5 | 6 | def self.client 7 | @_client ||= Aws::DynamoDB::Client.new(connect_config) 8 | end 9 | 10 | def self.connect_config 11 | config_keys = %w(endpoint access_key_id secret_access_key region) 12 | @connect_hash = {} 13 | 14 | config_keys.each do |config_key| 15 | if Dynamodb::Api::Config.send("#{config_key}?") 16 | @connect_hash[config_key.to_sym] = 17 | Dynamodb::Api::Config.send(config_key) 18 | end 19 | end 20 | 21 | @connect_hash 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "dynamodb/api" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | 15 | ENV['ACCESS_KEY'] ||= 'abcd' 16 | ENV['SECRET_KEY'] ||= '1234' 17 | 18 | Aws.config.update( 19 | region: 'ap-northeast-1', 20 | credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY']) 21 | ) 22 | 23 | Dynamodb::Api.config do |config| 24 | config.endpoint = 'http://dynamodb:8000' 25 | end 26 | 27 | IRB.start(__FILE__) 28 | -------------------------------------------------------------------------------- /lib/dynamodb/api/map/operator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Map 6 | class Operator # :nodoc: 7 | # Replace the Dynamodb Operators 8 | # @param k [String] The Dynamodb Operator 9 | # @return [String] 10 | def self.key(k) 11 | k = k.gsub(' ', '') 12 | case k 13 | when 'EQ' 14 | '=' 15 | when 'NE' 16 | '!=' 17 | when 'LE' 18 | '<=' 19 | when 'LT' 20 | '<' 21 | when 'GE' 22 | '>=' 23 | when 'GT' 24 | '>' 25 | else 26 | k 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/dynamodb/api/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dynamodb/api/config/options' 4 | 5 | module Dynamodb 6 | module Api 7 | module Config 8 | extend self 9 | extend Options 10 | 11 | option :access_key_id, default: nil 12 | option :secret_access_key, default: nil 13 | option :region, default: nil 14 | option :endpoint, default: nil 15 | option :retry_limit, default: 10 16 | option :table_name_prefix, default: '' 17 | option :index_name_prefix, default: '' 18 | 19 | # @param value [String] the table name 20 | # @return [String] the table name 21 | def build_table_name(value) 22 | return value unless table_name_prefix? 23 | "#{table_name_prefix}#{value}" 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dynamodb/api/relation/query_methods' 4 | 5 | module Dynamodb 6 | module Api 7 | module Relation # :nodoc: 8 | include QueryMethods 9 | attr_accessor :index_clause 10 | attr_accessor :from_clause 11 | attr_accessor :select_clause 12 | attr_accessor :order_clause 13 | attr_accessor :where_clause 14 | attr_accessor :filter_clause 15 | attr_accessor :attr_expression_attribute 16 | attr_accessor :limit_clause 17 | 18 | def expression_attribute 19 | if attr_expression_attribute.nil? 20 | self.attr_expression_attribute = Relation::ExpressionAttributeNames.new 21 | else 22 | attr_expression_attribute 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/dynamodb/api/relation/from_clause_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Relation::FromClause do 4 | describe '#initialize' do 5 | context 'table_name_prefix is empty' do 6 | before do 7 | Dynamodb::Api::Config.table_name_prefix = '' 8 | end 9 | 10 | let(:table_name) { 'table_name' } 11 | 12 | it 'returns a table name' do 13 | expect(Dynamodb::Api::Relation::FromClause.new(table_name).name). 14 | to eq table_name 15 | end 16 | end 17 | 18 | context 'table_name_prefix is not empty' do 19 | before do 20 | Dynamodb::Api::Config.table_name_prefix = 'prefix_' 21 | end 22 | 23 | let(:table_name) { 'table_name' } 24 | 25 | it 'returns a table name' do 26 | expect(Dynamodb::Api::Relation::FromClause.new(table_name).name). 27 | to eq('prefix_' + table_name) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | ruby: [2.4, 2.5, 2.6, 2.7] 9 | gemfile: [ aws_sdk_2_rails_5, aws_sdk_2_rails_6, aws_sdk_3_rails_5, aws_sdk_3_rails_6 ] 10 | exclude: 11 | - { ruby: 2.4, gemfile: aws_sdk_2_rails_6 } 12 | - { ruby: 2.4, gemfile: aws_sdk_3_rails_6 } 13 | runs-on: ubuntu-latest 14 | env: 15 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 16 | TEST_DYNAMODB_ENDPOINT: 'http://0.0.0.0:8000' 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true 23 | - run: bin/setup 24 | - run: docker-compose up -d 25 | - run: docker ps 26 | - run: bundle exec rubocop --fail-level=C 27 | - run: bundle exec rake spec 28 | - run: docker-compose down 29 | -------------------------------------------------------------------------------- /lib/dynamodb/api/config/options.rb: -------------------------------------------------------------------------------- 1 | module Dynamodb 2 | module Api 3 | module Config 4 | module Options 5 | def defaults 6 | @defaults ||= {} 7 | end 8 | 9 | def option(name, options = {}) 10 | defaults[name] = settings[name] = options[:default] 11 | 12 | class_eval <<-RUBY 13 | def #{name} 14 | settings[#{name.inspect}] 15 | end 16 | 17 | def #{name}=(value) 18 | settings[#{name.inspect}] = value 19 | end 20 | 21 | def #{name}? 22 | #{name} 23 | end 24 | 25 | def reset_#{name} 26 | settings[#{name.inspect}] = defaults[#{name.inspect}] 27 | end 28 | RUBY 29 | end 30 | 31 | def reset 32 | settings.replace(defaults) 33 | end 34 | 35 | def settings 36 | @settings ||= {} 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/dynamodb/api/update/attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Update 6 | class Attributes < Base # :nodoc: 7 | def remove_attributes(key, cols, table_name) 8 | update_item(key, cols, table_name) 9 | end 10 | 11 | private 12 | 13 | def build_update_expression 14 | str = @cols.each_with_object([]) do |k, ary| 15 | ary << "##{k}" 16 | end.join(', ') 17 | "REMOVE #{str}" 18 | end 19 | 20 | def build_expression_attribute_names 21 | @cols.each_with_object({}) do |k, h| 22 | h["##{k}"] = k 23 | end 24 | end 25 | 26 | def build_update_clause 27 | { 28 | key: @key, table_name: @table_name, 29 | update_expression: build_update_expression, 30 | expression_attribute_names: build_expression_attribute_names, 31 | } 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dynamodb/api/update/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Update 6 | class Base # :nodoc: 7 | def update_item(key, cols, table_name) 8 | @key = key 9 | @cols = cols 10 | @table_name = Dynamodb::Api::Config.build_table_name(table_name) 11 | 12 | client = Adapter.client 13 | client.update_item(build_update_clause) 14 | end 15 | 16 | private 17 | 18 | def build_update_expression 19 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 20 | end 21 | 22 | def build_expression_attribute_names 23 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 24 | end 25 | 26 | def build_expression_attribute_values 27 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 28 | end 29 | 30 | def build_update_clause 31 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 WalkerSumida 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'dynamodb/api' 3 | require 'support/dynamodb_helper' 4 | require 'pry' 5 | 6 | ENV['ACCESS_KEY'] ||= 'abcd' 7 | ENV['SECRET_KEY'] ||= '1234' 8 | 9 | Aws.config.update( 10 | region: 'ap-northeast-1', 11 | credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY']) 12 | ) 13 | 14 | Dynamodb::Api.config do |config| 15 | # Add `TEST_DYNAMODB_ENDPOINT`, as it may not be accessible on CI. 16 | # So specify `http://0.0.0.0:8000` on CI. 17 | endpoint = ENV['TEST_DYNAMODB_ENDPOINT'] || 'http://dynamodb:8000' 18 | config.endpoint = endpoint 19 | end 20 | 21 | RSpec.configure do |config| 22 | # Enable flags like --only-failures and --next-failure 23 | config.example_status_persistence_file_path = '.rspec_status' 24 | 25 | # Disable RSpec exposing methods globally on `Module` and `main` 26 | config.disable_monkey_patching! 27 | 28 | config.expect_with :rspec do |c| 29 | c.syntax = :expect 30 | end 31 | 32 | config.before :each do 33 | Dynamodb::Api.drop_tables 34 | Dynamodb::Api.config.table_name_prefix = '' 35 | Dynamodb::Api.config.index_name_prefix = '' 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/query_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | module QueryMethods # :nodoc: 7 | def from(value) 8 | self.from_clause = Relation::FromClause.new(value) 9 | self 10 | end 11 | 12 | def index(value) 13 | self.index_clause = Relation::GlobalSecondaryIndex.new(value) 14 | self 15 | end 16 | 17 | def select(value = nil) 18 | self.select_clause = Relation::SelectClause.new(value) 19 | self 20 | end 21 | 22 | def order(value = nil) 23 | self.order_clause = Relation::OrderClause.new(value) 24 | self 25 | end 26 | 27 | def where(expression) 28 | self.where_clause = Relation::WhereClause.new(expression) 29 | self 30 | end 31 | 32 | def filter(expression, values) 33 | self.filter_clause = Relation::FilterClause.new(expression, values) 34 | self 35 | end 36 | 37 | def limit(value) 38 | self.limit_clause = Relation::LimitClause.new(value) 39 | self 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dynamodb/api/update/item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Update 6 | class Item < Base # :nodoc: 7 | def update_item(key, cols, table_name) 8 | super(key, cols, table_name) 9 | end 10 | 11 | private 12 | 13 | def build_update_expression 14 | str = @cols.each_with_object([]) do |(k, _v), ary| 15 | ary << "##{k} = :#{k}" 16 | end.join(', ') 17 | "SET #{str}" 18 | end 19 | 20 | def build_expression_attribute_names 21 | @cols.each_with_object({}) do |(k, _v), h| 22 | h["##{k}"] = k 23 | end 24 | end 25 | 26 | def build_expression_attribute_values 27 | @cols.each_with_object({}) do |(k, v), h| 28 | h[":#{k}"] = v 29 | end 30 | end 31 | 32 | def build_update_clause 33 | { 34 | key: @key, table_name: @table_name, 35 | update_expression: build_update_expression, 36 | expression_attribute_names: build_expression_attribute_names, 37 | expression_attribute_values: build_expression_attribute_values, 38 | } 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/dynamodb/api/scan.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dynamodb/api/relation' 4 | 5 | module Dynamodb 6 | module Api 7 | class Scan < Base # :nodoc: 8 | def all 9 | result = Adapter.client.scan(build_input) 10 | @last_evaluated_key = result.last_evaluated_key 11 | result 12 | end 13 | 14 | def next 15 | return nil if @last_evaluated_key.blank? 16 | input = build_input 17 | input[:exclusive_start_key] = @last_evaluated_key 18 | result = Adapter.client.scan(input) 19 | @last_evaluated_key = result.last_evaluated_key 20 | result 21 | end 22 | 23 | private 24 | 25 | def build_input 26 | input = Aws::DynamoDB::Types::ScanInput.new 27 | input = base_input(input) 28 | input = build_filter_clause(input) 29 | input[:index_name] = index_clause.name if index_clause&.name 30 | input[:select] = select_name(select_clause) 31 | build_expression_attribute_names(input) 32 | input[:limit] = limit_clause.number if limit_clause&.number 33 | input 34 | end 35 | 36 | def base_input(input) 37 | input[:table_name] = from_clause.name 38 | input 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/dynamodb/api/relation/filter_clause_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Relation::FilterClause do 4 | describe '#initialize' do 5 | context 'when has not reserved words' do 6 | it 'returns filter expression and values' do 7 | filter_clause = Dynamodb::Api::Relation::FilterClause. 8 | new('model = :model', ':model': 'S2000') 9 | 10 | expect(filter_clause.expression).to eq('model = :model') 11 | expect(filter_clause.values).to eq(':model': 'S2000') 12 | expect(filter_clause.reserved_words).to eq([]) 13 | end 14 | end 15 | 16 | context 'when has reserved words' do 17 | it 'returns filter expression and values' do 18 | filter_clause = Dynamodb::Api::Relation::FilterClause. 19 | new('model = :model and #status = :status and #year = :year', 20 | ':model': 'S2000', '#status': 1, '#year': 2018) 21 | 22 | expect(filter_clause.expression). 23 | to eq('model = :model and #status = :status and #year = :year') 24 | expect(filter_clause.values). 25 | to eq(':model': 'S2000', '#status': 1, '#year': 2018) 26 | expect(filter_clause.reserved_words).to eq(['#status', '#year']) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/expression_attribute_names.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class ExpressionAttributeNames 7 | attr_accessor :names 8 | 9 | def initialize(names = {}) 10 | self.names = {} 11 | 12 | case names.class.to_s 13 | when 'String' 14 | add(names) 15 | when 'Array' 16 | names.each do |name| 17 | add(name) 18 | end 19 | when 'Hash' 20 | self.names = names 21 | else 22 | raise "#{names.class} is not support" 23 | end 24 | end 25 | 26 | def add(names) 27 | _names = formatting(names) 28 | self.names.merge!(_names) 29 | end 30 | 31 | private 32 | 33 | def formatting(names) 34 | _names = names.is_a?(Array) ? names : [names] 35 | _names.each_with_object({}) do |name, hash| 36 | next hash[name.to_sym] = remove_hash_tag(name) if name.is_a?(String) 37 | next hash.merge!(name) if name.is_a?(Hash) 38 | raise "#{name.class} is not support" 39 | end 40 | end 41 | 42 | def remove_hash_tag(name) 43 | name.gsub(/\#/, '') 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/dynamodb/api/put/item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Put::Item do 4 | before do 5 | DynamodbHelper.new.create_dummy_data 6 | end 7 | 8 | let(:table_name) { 'cars' } 9 | let(:item) do 10 | { id: '5', maker_id: 1, maker: 'Honda', model: 'NSX', release_date: 19900914 } 11 | end 12 | 13 | describe '#put_item' do 14 | subject { Dynamodb::Api::Put::Item.put_item(item, table_name) } 15 | 16 | let(:result) do 17 | query = Dynamodb::Api.query 18 | query.from('cars').index('index_maker_id_release_date') 19 | query.where(['maker_id', 'EQ', 1]) 20 | query.all.items.detect { |i| i['model'] == 'NSX' } 21 | end 22 | 23 | it 'works' do 24 | is_expected 25 | expect(result['model']).to eq('NSX') 26 | end 27 | end 28 | 29 | describe '#put_item table name prefix test' do 30 | subject { Dynamodb::Api::Put::Item.put_item(item, table_name) } 31 | 32 | let(:prefix) { 'prefix_' } 33 | 34 | before do 35 | Dynamodb::Api.config.table_name_prefix = prefix 36 | end 37 | 38 | it '(prefix) is added to table name' do 39 | mock = double('Adapter.client') 40 | allow(mock).to receive(:put_item) 41 | allow(Dynamodb::Api::Adapter).to receive(:client).and_return(mock) 42 | expect(mock).to receive(:put_item).with(item: item, table_name: "#{prefix}#{table_name}") 43 | 44 | is_expected 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /dynamodb-api.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'dynamodb/api/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'dynamodb-api' 7 | spec.version = Dynamodb::Api::VERSION 8 | spec.authors = ['WalkerSumida'] 9 | spec.email = ['walkersumida@gmail.com'] 10 | 11 | spec.summary = %q(aws dynamodb api) 12 | spec.description = %q(aws dynamodb api) 13 | spec.homepage = 'https://github.com/walkersumida/dynamodb-api' 14 | spec.license = 'MIT' 15 | 16 | # Specify which files should be added to the gem when it is released. 17 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 18 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 19 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 20 | end 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ['lib'] 24 | 25 | spec.add_runtime_dependency 'aws-sdk', '>= 2' 26 | spec.add_runtime_dependency('activesupport', '>= 5') 27 | 28 | spec.add_development_dependency 'rake', '>= 12.3.3' 29 | spec.add_development_dependency 'rspec', '~> 3.0' 30 | spec.add_development_dependency 'pry' 31 | spec.add_development_dependency 'rubocop-airbnb' 32 | spec.add_development_dependency 'appraisal' 33 | end 34 | -------------------------------------------------------------------------------- /spec/dynamodb/api/delete/item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Delete::Item do 4 | before do 5 | items = [ 6 | { 7 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 8 | }, 9 | { 10 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 11 | }, 12 | ] 13 | DynamodbHelper.new.create_dummy_data(items) 14 | end 15 | 16 | let(:table_name) { 'cars' } 17 | let(:item) do 18 | { id: '4' } 19 | end 20 | 21 | describe '#delete_item' do 22 | subject { Dynamodb::Api::Delete::Item.delete_item(item, table_name) } 23 | 24 | let(:results) do 25 | query = Dynamodb::Api.query 26 | query.from('cars').index('index_maker_id_release_date') 27 | query.where(['maker_id', 'EQ', 1]) 28 | query.all.items 29 | end 30 | 31 | it 'works' do 32 | is_expected 33 | expect(results.detect { |i| i['model'] == 'S2000' }).to eq(nil) 34 | expect(results.count).to eq(1) 35 | end 36 | end 37 | 38 | describe '#delete_item table name prefix test' do 39 | subject { Dynamodb::Api::Delete::Item.delete_item(item, table_name) } 40 | 41 | let(:prefix) { 'prefix_' } 42 | 43 | before do 44 | Dynamodb::Api.config.table_name_prefix = prefix 45 | end 46 | 47 | it '(prefix) is added to table name' do 48 | mock = double('Adapter.client') 49 | allow(mock).to receive(:delete_item) 50 | allow(Dynamodb::Api::Adapter).to receive(:client).and_return(mock) 51 | expect(mock).to receive(:delete_item).with(key: item, table_name: "#{prefix}#{table_name}") 52 | 53 | is_expected 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/dynamodb/api/update/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Update::Attributes do 4 | before do 5 | DynamodbHelper.new.create_dummy_data(items) 6 | end 7 | 8 | let(:items) do 9 | [ 10 | { 11 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 1, 12 | }, 13 | { 14 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 15 | }, 16 | ] 17 | end 18 | let(:table_name) { 'cars' } 19 | let(:key) do 20 | { id: '1' } 21 | end 22 | let(:attributes) do 23 | ['status'] 24 | end 25 | 26 | describe '#remove_attributes' do 27 | subject { Dynamodb::Api::Update::Attributes.new.remove_attributes(key, attributes, table_name) } 28 | 29 | let(:results) do 30 | query = Dynamodb::Api.query 31 | query.from('cars').index('index_maker_id_release_date') 32 | query.where(['maker_id', 'EQ', 1]).filter('#status = :status', ':status': 1) 33 | query.all.items 34 | end 35 | 36 | it 'works' do 37 | is_expected 38 | expect(results.count).to eq(1) 39 | end 40 | end 41 | 42 | describe '#remove_attributes table name prefix test' do 43 | subject { Dynamodb::Api::Update::Attributes.new.remove_attributes(key, attributes, table_name) } 44 | 45 | let(:prefix) { 'prefix_' } 46 | 47 | before do 48 | Dynamodb::Api.config.table_name_prefix = prefix 49 | end 50 | 51 | it '(prefix) is added to table name' do 52 | mock = double('Adapter.client') 53 | allow(mock).to receive(:update_item) 54 | allow(Dynamodb::Api::Adapter).to receive(:client).and_return(mock) 55 | expect(mock).to receive(:update_item). 56 | with(hash_including(table_name: "#{prefix}#{table_name}")) 57 | 58 | is_expected 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/dynamodb/api/query.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dynamodb/api/relation' 4 | 5 | module Dynamodb 6 | module Api 7 | class Query < Base # :nodoc: 8 | def all 9 | result = Adapter.client.query(build_input) 10 | @last_evaluated_key = result.last_evaluated_key 11 | result 12 | end 13 | 14 | def next 15 | return nil if @last_evaluated_key.blank? 16 | input = build_input 17 | input[:exclusive_start_key] = @last_evaluated_key 18 | result = Adapter.client.query(input) 19 | @last_evaluated_key = result.last_evaluated_key 20 | return nil if result.count.zero? 21 | result 22 | end 23 | 24 | private 25 | 26 | # Build options. 27 | # 28 | # @return [Aws::DynamoDB::Types::ScanInput] 29 | def build_input 30 | input = Aws::DynamoDB::Types::QueryInput.new 31 | input[:expression_attribute_values] = {} 32 | input = base_input(input) 33 | input = build_filter_clause(input) 34 | input[:scan_index_forward] = order_direct(order_clause) 35 | input[:select] = select_name(select_clause) 36 | build_expression_attribute_names(input) 37 | input[:limit] = limit_clause.number if limit_clause&.number 38 | input 39 | end 40 | 41 | # Build required options. 42 | # `key_condition_expression`: e.g. "Artist = :a" 43 | # `expression_attribute_values`: e.g. { ":a" => "No One You Know" } 44 | # 45 | # @param input [Aws::DynamoDB::Types::ScanInput] 46 | # @return [Aws::DynamoDB::Types::ScanInput] 47 | def base_input(input) 48 | input[:table_name] = from_clause.name 49 | input[:index_name] = index_clause.name 50 | input[:key_condition_expression] = where_clause.key_condition 51 | input[:expression_attribute_values]. 52 | merge!(where_clause.attribute_values) 53 | input 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/dynamodb/api/update/item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Update::Item do 4 | before do 5 | DynamodbHelper.new.create_dummy_data(items) 6 | end 7 | 8 | let(:items) do 9 | [ 10 | { 11 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 12 | }, 13 | { 14 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 15 | }, 16 | ] 17 | end 18 | let(:table_name) { 'cars' } 19 | let(:key) do 20 | { id: '1' } 21 | end 22 | let(:item) do 23 | { status: 1, new_col: 'new' } 24 | end 25 | 26 | describe '#update_item' do 27 | subject { Dynamodb::Api::Update::Item.new.update_item(key, item, table_name) } 28 | 29 | let(:results) do 30 | query = Dynamodb::Api.query 31 | query.from('cars').index('index_maker_id_release_date') 32 | query.where(['maker_id', 'EQ', 1]).filter('#status = :status', ':status': 1) 33 | query.all.items 34 | end 35 | 36 | it 'works' do 37 | is_expected 38 | expect(results.detect { |i| i['model'] == 'Accord' }['model']).to eq(items[0][:model]) 39 | expect(results.detect { |i| i['model'] == 'Accord' }['new_col']).to eq('new') 40 | expect(results.count).to eq(2) 41 | end 42 | end 43 | 44 | describe '#update_item table name prefix test' do 45 | subject { Dynamodb::Api::Update::Item.new.update_item(key, item, table_name) } 46 | 47 | let(:prefix) { 'prefix_' } 48 | 49 | before do 50 | Dynamodb::Api.config.table_name_prefix = prefix 51 | end 52 | 53 | it '(prefix) is added to table name' do 54 | mock = double('Adapter.client') 55 | allow(mock).to receive(:update_item) 56 | allow(Dynamodb::Api::Adapter).to receive(:client).and_return(mock) 57 | expect(mock).to receive(:update_item). 58 | with(hash_including(table_name: "#{prefix}#{table_name}")) 59 | 60 | is_expected 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | --- 10 | 11 | ## [0.9.1] - 2020-11-21 12 | ### Security 13 | - [#59](https://github.com/walkersumida/dynamodb-api/pull/59) Update rake version 14 | 15 | ### Removed 16 | - [#57](https://github.com/walkersumida/dynamodb-api/pull/57) Unsupport ruby2.3 and activesupport4 17 | 18 | ## [0.9.0] - 2020-02-24 19 | ### Fixed 20 | - [#56](https://github.com/walkersumida/dynamodb-api/pull/56) Make the `begins_with` and `between` available 21 | 22 | ## [0.8.1] - 2019-06-03 23 | ### Fixed 24 | - [#49](https://github.com/walkersumida/dynamodb-api/pull/49) not be added table name prefix 25 | - [#48](https://github.com/walkersumida/dynamodb-api/pull/48) Improvement DX 26 | 27 | ## [0.8.0] - 2019-03-03 28 | ### Added 29 | - [#39](https://github.com/walkersumida/dynamodb-api/pull/39) `next` method 30 | 31 | ## [0.7.0] - 2018-12-23 32 | ### Added 33 | - [#34](https://github.com/walkersumida/dynamodb-api/pull/34) `scan` method 34 | 35 | ## [0.6.2] - 2018-12-13 36 | ### Added 37 | - [#32](https://github.com/walkersumida/dynamodb-api/pull/32) `remove_attributes` method 38 | 39 | ## [0.6.1] - 2018-11-22 40 | ### Added 41 | - [#30](https://github.com/walkersumida/dynamodb-api/pull/30) `retry_limit` option(default: 10) 42 | 43 | ## [0.6.0] - 2018-11-03 44 | ### Added 45 | - [#25](https://github.com/walkersumida/dynamodb-api/pull/25) `update` method 46 | - [#24](https://github.com/walkersumida/dynamodb-api/pull/24) `delete` method 47 | - [#23](https://github.com/walkersumida/dynamodb-api/pull/23) `query` method 48 | 49 | ## [0.5.0] - 2018-09-30 50 | ### Added 51 | - [#21](https://github.com/walkersumida/dynamodb-api/pull/21) Automatically add Expression Attribute Names 52 | 53 | ### Removed 54 | - [#21](https://github.com/walkersumida/dynamodb-api/pull/21) `ex_attr` method 55 | -------------------------------------------------------------------------------- /lib/dynamodb/api.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | require 'active_support' 3 | require 'active_support/core_ext' 4 | 5 | require 'dynamodb/api/version' 6 | require 'dynamodb/api/config' 7 | require 'dynamodb/api/adapter' 8 | require 'dynamodb/api/base' 9 | require 'dynamodb/api/query' 10 | require 'dynamodb/api/scan' 11 | require 'dynamodb/api/relation' 12 | require 'dynamodb/api/relation/query_methods' 13 | require 'dynamodb/api/relation/from_clause' 14 | require 'dynamodb/api/relation/select_clause' 15 | require 'dynamodb/api/relation/order_clause' 16 | require 'dynamodb/api/relation/where_clause' 17 | require 'dynamodb/api/relation/filter_clause' 18 | require 'dynamodb/api/relation/global_secondary_index' 19 | require 'dynamodb/api/relation/expression_attribute_names' 20 | require 'dynamodb/api/relation/limit_clause' 21 | require 'dynamodb/api/put/item' 22 | require 'dynamodb/api/delete/tables' 23 | require 'dynamodb/api/delete/item' 24 | require 'dynamodb/api/map/operator' 25 | require 'dynamodb/api/update/base' 26 | require 'dynamodb/api/update/item' 27 | require 'dynamodb/api/update/attributes' 28 | 29 | module Dynamodb 30 | module Api # :nodoc: 31 | module_function 32 | 33 | def config 34 | block_given? ? yield(Dynamodb::Api::Config) : Dynamodb::Api::Config 35 | end 36 | 37 | def adapter 38 | @adapter ||= Dynamodb::Api::Adapter.new 39 | end 40 | 41 | def drop_tables 42 | Delete::Tables.delete_tables 43 | end 44 | 45 | def scan 46 | Scan.new 47 | end 48 | 49 | def query 50 | Query.new 51 | end 52 | 53 | def insert(table_name, value) 54 | # TODO: BatchWriteItem 55 | Put::Item.put_item(value, table_name) 56 | end 57 | 58 | def update(table_name, key, value) 59 | Update::Item.new.update_item(key, value, table_name) 60 | end 61 | 62 | def delete(table_name, key) 63 | Delete::Item.delete_item(key, table_name) 64 | end 65 | 66 | def remove_attributes(table_name, key, attrs) 67 | Update::Attributes.new.remove_attributes(key, attrs, table_name) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/dynamodb/api/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dynamodb/api/relation' 4 | 5 | module Dynamodb 6 | module Api 7 | class Base # :nodoc: 8 | include Relation 9 | attr_accessor :last_evaluated_key 10 | 11 | def all 12 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 13 | end 14 | 15 | def next 16 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 17 | end 18 | 19 | private 20 | 21 | def build_input 22 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 23 | end 24 | 25 | def base_params 26 | raise NotImplementedError, "You must implement #{self.class}##{__method__}" 27 | end 28 | 29 | def order_direct(clause) 30 | clause&.direct ? clause.direct : OrderClause.new.direct 31 | end 32 | 33 | def select_name(clause) 34 | clause&.name ? clause.name : SelectClause.new.name 35 | end 36 | 37 | # Build filter clause. 38 | # `filter_expression`: e.g. "Artist = :a" 39 | # `expression_attribute_values`: e.g. { ":a" => "No One You Know" } 40 | # 41 | # @param input [Aws::DynamoDB::Types::ScanInput] 42 | # @return [Aws::DynamoDB::Types::ScanInput] 43 | def build_filter_clause(input) 44 | return input if filter_clause&.expression.blank? 45 | input[:filter_expression] = filter_clause.expression 46 | if filter_clause.values.present? 47 | if input[:expression_attribute_values].nil? 48 | input[:expression_attribute_values] = {} 49 | end 50 | input[:expression_attribute_values]. 51 | merge!(filter_clause.values) 52 | end 53 | input 54 | end 55 | 56 | def build_expression_attribute_names(input) 57 | if filter_clause&.reserved_words.present? 58 | expression_attribute.add(filter_clause.reserved_words) 59 | end 60 | if expression_attribute.names.present? 61 | input[:expression_attribute_names] = expression_attribute.names 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/dynamodb/api/relation/expression_attribute_names_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Relation::ExpressionAttributeNames do 4 | describe '#initialize' do 5 | context 'arg is hash' do 6 | it 'returns expression attribute names' do 7 | names = { '#status': 'status' } 8 | 9 | expect( 10 | Dynamodb::Api::Relation::ExpressionAttributeNames.new(names).names 11 | ).to eq({ '#status': 'status' }) 12 | end 13 | end 14 | 15 | context 'arg is string' do 16 | it 'returns expression attribute names' do 17 | names = '#status' 18 | 19 | expect( 20 | Dynamodb::Api::Relation::ExpressionAttributeNames.new(names).names 21 | ).to eq({ '#status': 'status' }) 22 | end 23 | end 24 | 25 | context 'arg is array' do 26 | it 'returns expression attribute names' do 27 | names = ['#status', '#year'] 28 | 29 | expect( 30 | Dynamodb::Api::Relation::ExpressionAttributeNames.new(names).names 31 | ).to eq({ '#status': 'status', '#year': 'year' }) 32 | end 33 | end 34 | end 35 | 36 | describe '#add' do 37 | context 'arg is string' do 38 | it 'returns expression attribute names' do 39 | attr_names = { '#status': 'status' } 40 | add_name = '#year' 41 | expression_attribute_names = Dynamodb::Api::Relation::ExpressionAttributeNames. 42 | new(attr_names) 43 | expression_attribute_names.add(add_name) 44 | 45 | expect( 46 | expression_attribute_names.names 47 | ).to eq({ '#status': 'status', '#year': 'year' }) 48 | end 49 | end 50 | 51 | context 'arg is hash' do 52 | it 'returns expression attribute names' do 53 | attr_names = { '#status': 'status' } 54 | add_name = { '#year': 'year' } 55 | expression_attribute_names = Dynamodb::Api::Relation::ExpressionAttributeNames. 56 | new(attr_names) 57 | expression_attribute_names.add(add_name) 58 | 59 | expect( 60 | expression_attribute_names.names 61 | ).to eq({ '#status': 'status', '#year': 'year' }) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/dynamodb/api/scan_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dynamodb::Api::Scan do 2 | before do 3 | items = [ 4 | { 5 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 6 | }, 7 | { 8 | id: '2', maker_id: 2, maker: 'Toyota', model: 'CROWN', release_date: 19550101, status: 0, 9 | }, 10 | { 11 | id: '3', maker_id: 3, maker: 'Tesla', model: 'Model S', release_date: 20120601, status: 0, 12 | }, 13 | { 14 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 15 | }, 16 | ] 17 | DynamodbHelper.new.create_dummy_data(items) 18 | end 19 | 20 | describe '#all' do 21 | context 'select clause' do 22 | it 'works' do 23 | scan = Dynamodb::Api.scan 24 | scan.from('cars'). 25 | select('ALL_ATTRIBUTES') 26 | items = scan.all.items 27 | expect(items.count).to eq(4) 28 | end 29 | end 30 | 31 | context 'limit clause' do 32 | it 'works' do 33 | scan = Dynamodb::Api.scan 34 | scan.from('cars'). 35 | limit(1) 36 | items = scan.all.items 37 | expect(items.count).to eq(1) 38 | end 39 | end 40 | 41 | context 'filter clause' do 42 | it 'works' do 43 | scan = Dynamodb::Api.scan 44 | scan.from('cars'). 45 | filter('model = :model', ':model': 'S2000') 46 | items = scan.all.items 47 | expect(items.count).to eq(1) 48 | end 49 | end 50 | 51 | context 'index clause' do 52 | it 'works(order: maker_id asc, release_date asc)' do 53 | scan = Dynamodb::Api.scan 54 | scan.from('cars'). 55 | index('index_maker_id_release_date') 56 | items = scan.all.items 57 | expect(items.map { |i| i['id'] }).to eq(%w(1 4 2 3)) 58 | end 59 | end 60 | 61 | context 'expression attribute names' do 62 | it 'works' do 63 | scan = Dynamodb::Api.scan 64 | scan.from('cars'). 65 | filter('#status = :status', ':status': 1) 66 | items = scan.all.items 67 | expect(items.count).to eq(1) 68 | end 69 | end 70 | end 71 | 72 | describe '#next' do 73 | context 'exists last_evaluated_key' do 74 | it 'returns next items' do 75 | scan = Dynamodb::Api.scan 76 | scan.from('cars'). 77 | limit(2) 78 | result = scan.all 79 | expect(result.items.map { |i| i['id'] }).to eq(%w(1 4)) 80 | result = scan.next 81 | expect(result.items.map { |i| i['id'] }).to eq(%w(3 2)) 82 | end 83 | end 84 | 85 | context 'not exists last_evaluated_key' do 86 | it 'returns nil' do 87 | scan = Dynamodb::Api.scan 88 | scan.from('cars') 89 | _result = scan.all 90 | result = scan.next 91 | expect(result).to be nil 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at walkersumida@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/dynamodb/api/relation/where_clause_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dynamodb::Api::Relation::WhereClause do 4 | describe '#initialize' do 5 | context 'single condition' do 6 | context 'value type is array' do 7 | let(:key_condition) do 8 | ['maker', '=', %w(Honda)] 9 | end 10 | 11 | it 'returns conditions for dynamodb' do 12 | expect( 13 | Dynamodb::Api::Relation::WhereClause. 14 | new(key_condition).key_condition 15 | ).to eq('maker = :maker') 16 | expect( 17 | Dynamodb::Api::Relation::WhereClause. 18 | new(key_condition).attribute_values 19 | ).to eq({ ':maker' => 'Honda' }) 20 | end 21 | end 22 | 23 | context 'value type is string' do 24 | let(:key_condition) do 25 | [%w(maker = Honda)] 26 | end 27 | 28 | it 'returns conditions for dynamodb' do 29 | expect( 30 | Dynamodb::Api::Relation::WhereClause. 31 | new(key_condition).key_condition 32 | ).to eq('maker = :maker') 33 | expect( 34 | Dynamodb::Api::Relation::WhereClause. 35 | new(key_condition).attribute_values 36 | ).to eq({ ':maker' => 'Honda' }) 37 | end 38 | end 39 | 40 | context 'with begins_with operator' do 41 | let(:key_condition) do 42 | [%w(maker begins_with Hon)] 43 | end 44 | 45 | it 'returns conditions for dynamodb' do 46 | expect( 47 | Dynamodb::Api::Relation::WhereClause. 48 | new(key_condition).key_condition 49 | ).to eq('begins_with(maker, :maker)') 50 | expect( 51 | Dynamodb::Api::Relation::WhereClause. 52 | new(key_condition).attribute_values 53 | ).to eq({ ':maker' => 'Hon' }) 54 | end 55 | end 56 | 57 | context 'with between operator' do 58 | let(:key_condition) do 59 | [['release_date', 'between', [19_980_101, 20_120_601]]] 60 | end 61 | 62 | it 'returns conditions for dynamodb' do 63 | expect( 64 | Dynamodb::Api::Relation::WhereClause. 65 | new(key_condition).key_condition 66 | ).to eq('release_date between :from_release_date AND :to_release_date') 67 | expect( 68 | Dynamodb::Api::Relation::WhereClause. 69 | new(key_condition).attribute_values 70 | ).to eq({ 71 | ':from_release_date' => 19_980_101, 72 | ':to_release_date' => 20_120_601, 73 | }) 74 | end 75 | end 76 | end 77 | 78 | context 'multiple conditions' do 79 | context 'value type is array' do 80 | let(:key_condition) do 81 | [%w(maker = Honda), ['release_date', '>=', 19_980_101]] 82 | end 83 | 84 | it 'returns conditions for dynamodb' do 85 | expect( 86 | Dynamodb::Api::Relation::WhereClause. 87 | new(key_condition).key_condition 88 | ).to eq('maker = :maker AND release_date >= :release_date') 89 | expect( 90 | Dynamodb::Api::Relation::WhereClause. 91 | new(key_condition).attribute_values 92 | ).to eq({ 93 | ':maker' => 'Honda', 94 | ':release_date' => 19_980_101, 95 | }) 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/dynamodb/api/relation/where_clause.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dynamodb 4 | module Api 5 | module Relation 6 | class WhereClause # :nodoc: 7 | attr_reader :key_condition 8 | attr_reader :attribute_values 9 | 10 | KEY = 0 11 | VALUE = 2 12 | OPERATOR = 1 13 | FROM = 0 14 | TO = 1 15 | 16 | # @param conditions [Array] 17 | def initialize(conditions) 18 | built_conditions = build(conditions) 19 | @key_condition = built_conditions[:key_conditions] 20 | @attribute_values = built_conditions[:attribute_values] 21 | end 22 | 23 | private 24 | 25 | # Build where clause. 26 | # 27 | # @param conditions [Array] 28 | def build(conditions) 29 | init = { key_conditions: [], attribute_values: {} } 30 | conditions = format_conditions(conditions) 31 | built = conditions.each_with_object(init) do |c, h| 32 | h[:key_conditions] << 33 | format_key_condition(c[KEY], c[OPERATOR]) 34 | h[:attribute_values]. 35 | merge!(format_values(c[KEY], c[OPERATOR], c[VALUE])) 36 | end 37 | built[:key_conditions] = 38 | built[:key_conditions].join(' AND ') 39 | built 40 | end 41 | 42 | # Format to the `key_condition_expression`. 43 | # e.g. 44 | # `format_key_condition("Artist", "=")` => 45 | # `"Artist = :Artist"` 46 | # 47 | # `format_key_condition("Artist", "begins_with")` => 48 | # `"begins_with(Artist, :Artist)"` 49 | # 50 | # `format_key_condition("Year", "between")` => 51 | # `"Year between :from_Year and :to_Year"` 52 | # 53 | # @param key [String] a attribute name 54 | # @param operator [String] a operator 55 | # @return [String] Formatted expression. 56 | def format_key_condition(key, operator) 57 | case operator.downcase 58 | when 'begins_with' 59 | "#{operator.downcase}(#{key}, :#{key})" 60 | when 'between' 61 | "#{key} #{operator.downcase}" \ 62 | " :from_#{key} AND :to_#{key}" 63 | else 64 | "#{key} #{Map::Operator.key(operator)} :#{key}" 65 | end 66 | end 67 | 68 | # Format to the `expression_attribute_values` 69 | # e.g. 70 | # `format_values("Artist", "=", "blue")` => 71 | # `{ ":Artist" => "blue" }` 72 | # 73 | # `format_values("Year", "between", [1999, 2020])` => 74 | # `{ ":from_Year" => 1999, ":to_Year" => 2020 }` 75 | # 76 | # @param key [String] a attribute name 77 | # @param operator [String] a operator 78 | # @param values [Object] values 79 | # @return [Hash] Formatted expression. 80 | def format_values(key, operator, values) 81 | case operator.downcase 82 | when 'between' 83 | { 84 | ":from_#{key}" => values[FROM], 85 | ":to_#{key}" => values[TO], 86 | } 87 | else 88 | { 89 | ":#{key}" => values.is_a?(Array) ? values[0] : values, 90 | } 91 | end 92 | end 93 | 94 | def format_conditions(conditions) 95 | return [conditions] unless conditions[0].is_a?(Array) 96 | conditions 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/support/dynamodb_helper.rb: -------------------------------------------------------------------------------- 1 | class DynamodbHelper 2 | attr_writer :items 3 | 4 | def create_dummy_data(items = nil) 5 | self.items = items 6 | create_table 7 | create_items 8 | end 9 | 10 | def delete_dummy_data 11 | delete_tables 12 | end 13 | 14 | private 15 | 16 | def delete_tables 17 | client = Dynamodb::Api::Adapter.client 18 | table_names = client.list_tables[:table_names] 19 | table_names.each do |table_name| 20 | client.delete_table(table_name: table_name) 21 | end 22 | end 23 | 24 | def create_table 25 | client = Dynamodb::Api::Adapter.client 26 | client.create_table( 27 | attribute_definitions: attribute_definitions, 28 | key_schema: key_schema, 29 | global_secondary_indexes: global_secondary_indexes, 30 | provisioned_throughput: provisioned_throughput, 31 | table_name: 'cars' 32 | ) 33 | end 34 | 35 | def create_items 36 | client = Dynamodb::Api::Adapter.client 37 | (@items || default_items).each do |item| 38 | client.put_item( 39 | item: item, 40 | table_name: 'cars' 41 | ) 42 | end 43 | end 44 | 45 | def default_items 46 | [ 47 | { id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508 }, 48 | { id: '2', maker_id: 2, maker: 'Toyota', model: 'CROWN', release_date: 19550101 }, 49 | { id: '3', maker_id: 3, maker: 'Tesla', model: 'Model S', release_date: 20120601 }, 50 | { id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101 }, 51 | ] 52 | end 53 | 54 | # MEMO: key_schema, indexで使用する属性だけ記載する 55 | def attribute_definitions 56 | [ 57 | { 58 | attribute_name: 'id', 59 | attribute_type: 'S', 60 | }, 61 | { 62 | attribute_name: 'maker_id', 63 | attribute_type: 'N', 64 | }, 65 | { 66 | attribute_name: 'release_date', 67 | attribute_type: 'N', 68 | }, 69 | { 70 | attribute_name: 'model', 71 | attribute_type: 'S', 72 | }, 73 | ] 74 | end 75 | 76 | def key_schema 77 | [ 78 | { 79 | attribute_name: 'id', 80 | key_type: 'HASH', 81 | }, 82 | ] 83 | end 84 | 85 | def global_secondary_indexes 86 | [ 87 | { 88 | index_name: 'index_maker_id_release_date', 89 | key_schema: [ 90 | { 91 | attribute_name: 'maker_id', 92 | key_type: 'HASH', 93 | }, 94 | { 95 | attribute_name: 'release_date', 96 | key_type: 'RANGE', 97 | }, 98 | ], 99 | projection: { 100 | projection_type: 'ALL', 101 | }, 102 | provisioned_throughput: { 103 | read_capacity_units: 5, 104 | write_capacity_units: 5, 105 | }, 106 | }, 107 | { 108 | index_name: 'index_maker_id_model', 109 | key_schema: [ 110 | { 111 | attribute_name: 'maker_id', 112 | key_type: 'HASH', 113 | }, 114 | { 115 | attribute_name: 'model', 116 | key_type: 'RANGE', 117 | }, 118 | ], 119 | projection: { 120 | projection_type: 'ALL', 121 | }, 122 | provisioned_throughput: { 123 | read_capacity_units: 5, 124 | write_capacity_units: 5, 125 | }, 126 | }, 127 | ] 128 | end 129 | 130 | def provisioned_throughput 131 | { 132 | read_capacity_units: 5, 133 | write_capacity_units: 5, 134 | } 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/dynamodb/api/query_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dynamodb::Api::Query do 2 | before do 3 | items = [ 4 | { 5 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 6 | }, 7 | { 8 | id: '2', maker_id: 2, maker: 'Toyota', model: 'CROWN', release_date: 19550101, status: 0, 9 | }, 10 | { 11 | id: '3', maker_id: 3, maker: 'Tesla', model: 'Model S', release_date: 20120601, status: 0, 12 | }, 13 | { 14 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 15 | }, 16 | ] 17 | DynamodbHelper.new.create_dummy_data(items) 18 | end 19 | 20 | describe '#all' do 21 | context 'where clause' do 22 | it 'works(only hash key)' do 23 | query = Dynamodb::Api.query 24 | query.from('cars').index('index_maker_id_release_date'). 25 | where(['maker_id', '=', 1]) 26 | items = query.all.items 27 | expect(items.count).to eq(2) 28 | end 29 | 30 | it 'works(hash/range key)' do 31 | query = Dynamodb::Api.query 32 | query.from('cars').index('index_maker_id_release_date'). 33 | where([['maker_id', '=', 1], ['release_date', '>=', 19980101]]) 34 | items = query.all.items 35 | expect(items.count).to eq(1) 36 | end 37 | 38 | it 'works(using BEGINS_WITH)' do 39 | query = Dynamodb::Api.query 40 | query.from('cars').index('index_maker_id_model'). 41 | where([['maker_id', '=', 1], ['model', 'begins_with', 'S2']]) 42 | items = query.all.items 43 | expect(items.count).to eq(1) 44 | end 45 | 46 | it 'works(using BETWEEN)' do 47 | query = Dynamodb::Api.query 48 | query.from('cars').index('index_maker_id_release_date'). 49 | where( 50 | [ 51 | ['maker_id', '=', 1], 52 | ['release_date', 'between', [19550101, 19980101]], 53 | ] 54 | ) 55 | items = query.all.items 56 | expect(items.count).to eq(2) 57 | end 58 | end 59 | 60 | context 'limit clause' do 61 | it 'works' do 62 | query = Dynamodb::Api.query 63 | query.from('cars').index('index_maker_id_release_date'). 64 | where(['maker_id', '=', 1]). 65 | limit(1) 66 | items = query.all.items 67 | expect(items.count).to eq(1) 68 | end 69 | end 70 | 71 | context 'filter clause' do 72 | it 'works' do 73 | query = Dynamodb::Api.query 74 | query.from('cars').index('index_maker_id_release_date'). 75 | where(['maker_id', '=', 1]). 76 | filter('model = :model', ':model': 'S2000') 77 | items = query.all.items 78 | expect(items.count).to eq(1) 79 | end 80 | end 81 | 82 | context 'expression attribute names' do 83 | it 'works' do 84 | query = Dynamodb::Api.query 85 | query.from('cars').index('index_maker_id_release_date'). 86 | where(['maker_id', '=', 1]). 87 | filter('#status = :status', ':status': 1) 88 | items = query.all.items 89 | expect(items.count).to eq(1) 90 | end 91 | end 92 | end 93 | 94 | describe '#next' do 95 | context 'exists last_evaluated_key' do 96 | it 'returns next items' do 97 | query = Dynamodb::Api.query 98 | query.from('cars').index('index_maker_id_release_date'). 99 | where(['maker_id', '=', 1]). 100 | limit(1) 101 | result = query.all 102 | expect(result.items.map { |i| i['id'] }).to eq(%w(4)) 103 | result = query.next 104 | expect(result.items.map { |i| i['id'] }).to eq(%w(1)) 105 | end 106 | end 107 | 108 | context 'not exists last_evaluated_key' do 109 | it 'returns nil' do 110 | query = Dynamodb::Api.query 111 | query.from('cars').index('index_maker_id_release_date'). 112 | where(['maker_id', '=', 1]). 113 | limit(2) 114 | _result = query.all 115 | result = query.next 116 | expect(result).to be nil 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/dynamodb/api_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Dynamodb::Api do 2 | it 'has a version number' do 3 | expect(Dynamodb::Api::VERSION).not_to be nil 4 | end 5 | 6 | describe '#config' do 7 | context 'when block_given is true' do 8 | before do 9 | Dynamodb::Api.config do |config| 10 | config.endpoint = 'http://1.1.1.1:8000' 11 | end 12 | end 13 | 14 | it 'works' do 15 | expect(Dynamodb::Api::Config.endpoint).to eq('http://1.1.1.1:8000') 16 | end 17 | end 18 | 19 | context 'when block_given is false' do 20 | it 'works' do 21 | expect(Dynamodb::Api.config).to eq(Dynamodb::Api::Config) 22 | end 23 | end 24 | end 25 | 26 | describe '#adapter' do 27 | it 'returns a Dynamodb::Api::Adapter class' do 28 | expect(Dynamodb::Api.adapter.class).to eq(Dynamodb::Api::Adapter) 29 | end 30 | end 31 | 32 | describe '#query' do 33 | it 'returns a Dynamodb::Api::Query class' do 34 | expect(Dynamodb::Api.query.class).to eq(Dynamodb::Api::Query) 35 | end 36 | end 37 | 38 | describe '#insert' do 39 | subject { Dynamodb::Api.insert(table_name, item) } 40 | 41 | before do 42 | DynamodbHelper.new.create_dummy_data 43 | end 44 | 45 | let(:table_name) { 'cars' } 46 | let(:item) do 47 | { id: '5', maker_id: 1, maker: 'Honda', model: 'NSX', release_date: 19900914 } 48 | end 49 | let(:result) do 50 | query = Dynamodb::Api.query 51 | query.from('cars').index('index_maker_id_release_date') 52 | query.where(['maker_id', 'EQ', 1]) 53 | query.all.items.detect { |i| i['model'] == 'NSX' } 54 | end 55 | 56 | it 'works' do 57 | is_expected 58 | expect(result['model']).to eq('NSX') 59 | end 60 | end 61 | 62 | describe '#update' do 63 | subject { Dynamodb::Api.update(table_name, key, item) } 64 | 65 | before do 66 | DynamodbHelper.new.create_dummy_data(items) 67 | end 68 | 69 | let(:items) do 70 | [ 71 | { 72 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 73 | }, 74 | { 75 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 76 | }, 77 | ] 78 | end 79 | let(:table_name) { 'cars' } 80 | let(:key) do 81 | { id: '1' } 82 | end 83 | let(:item) do 84 | { status: 1, new_col: 'new' } 85 | end 86 | let(:results) do 87 | query = Dynamodb::Api.query 88 | query.from('cars').index('index_maker_id_release_date') 89 | query.where(['maker_id', 'EQ', 1]).filter('#status = :status', ':status': 1) 90 | query.all.items 91 | end 92 | 93 | it 'works' do 94 | is_expected 95 | expect(results.detect { |i| i['model'] == 'Accord' }['model']).to eq(items[0][:model]) 96 | expect(results.detect { |i| i['model'] == 'Accord' }['new_col']).to eq('new') 97 | expect(results.count).to eq(2) 98 | end 99 | end 100 | 101 | describe '#delete' do 102 | subject { Dynamodb::Api.delete(table_name, item) } 103 | 104 | before do 105 | items = [ 106 | { 107 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 0, 108 | }, 109 | { 110 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 111 | }, 112 | ] 113 | DynamodbHelper.new.create_dummy_data(items) 114 | end 115 | 116 | let(:table_name) { 'cars' } 117 | let(:item) do 118 | { id: '4' } 119 | end 120 | let(:results) do 121 | query = Dynamodb::Api.query 122 | query.from('cars').index('index_maker_id_release_date') 123 | query.where(['maker_id', 'EQ', 1]) 124 | query.all.items 125 | end 126 | 127 | it 'works' do 128 | is_expected 129 | expect(results.detect { |i| i['model'] == 'S2000' }).to eq(nil) 130 | expect(results.count).to eq(1) 131 | end 132 | end 133 | 134 | describe '#remove_attributes' do 135 | subject { Dynamodb::Api.remove_attributes(table_name, key, attributes) } 136 | 137 | before do 138 | DynamodbHelper.new.create_dummy_data(items) 139 | end 140 | 141 | let(:items) do 142 | [ 143 | { 144 | id: '1', maker_id: 1, maker: 'Honda', model: 'Accord', release_date: 19760508, status: 1, 145 | }, 146 | { 147 | id: '4', maker_id: 1, maker: 'Honda', model: 'S2000', release_date: 19980101, status: 1, 148 | }, 149 | ] 150 | end 151 | let(:table_name) { 'cars' } 152 | let(:key) do 153 | { id: '1' } 154 | end 155 | let(:attributes) do 156 | ['status'] 157 | end 158 | let(:results) do 159 | query = Dynamodb::Api.query 160 | query.from('cars').index('index_maker_id_release_date') 161 | query.where(['maker_id', 'EQ', 1]).filter('#status = :status', ':status': 1) 162 | query.all.items 163 | end 164 | 165 | it 'works' do 166 | is_expected 167 | expect(results.count).to eq(1) 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamodb::Api 2 | 3 | ![Build Status](https://github.com/walkersumida/dynamodb-api/workflows/build/badge.svg?branch=master) 4 | [![Gem Version](https://badge.fury.io/rb/dynamodb-api.svg)](https://badge.fury.io/rb/dynamodb-api) 5 | 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | ```ruby 11 | gem 'dynamodb-api' 12 | ``` 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install dynamodb-api 21 | 22 | ## Configuration 23 | 24 | ### Rails 25 | 26 | `config/initializers/dynamodb_api.rb` 27 | 28 | ```ruby 29 | Dynamodb::Api.config do |config| 30 | config.access_key_id = '' 31 | config.secret_access_key = '' 32 | config.region = '' 33 | config.table_name_prefix = '' 34 | config.index_name_prefix = '' 35 | end 36 | ``` 37 | 38 | ### Other 39 | 40 | ```ruby 41 | Dynamodb::Api.config.access_key_id = '' 42 | Dynamodb::Api.config.secret_access_key = '' 43 | Dynamodb::Api.config.region = '' 44 | Dynamodb::Api.config.table_name_prefix = '' 45 | Dynamodb::Api.config.index_name_prefix = '' 46 | ``` 47 | 48 | ## How to use 49 | e.g. 50 | 51 | cars table. 52 | 53 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 54 | |:---|:---|:---|:---|:---| 55 | |1 |1 |Accord |0.19760508e8 |0 | 56 | |2 |2 |CROWN |0.19550101e8 |0 | 57 | |3 |3 |Model S |0.20120601e8 |0 | 58 | |4 |1 |S2000 |0.19980101e8 |1 | 59 | 60 | ### Scan 61 | 62 | Scan returns items in random order. 63 | 64 | ```ruby 65 | scan = Dynamodb::Api.scan 66 | scan.from('cars') 67 | items = scan.all.items 68 | ``` 69 | 70 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 71 | |:---|:---|:---|:---|:---| 72 | |1 |1 |Accord |0.19760508e8 |0 | 73 | |2 |2 |CROWN |0.19550101e8 |0 | 74 | |3 |3 |Model S |0.20120601e8 |0 | 75 | |4 |1 |S2000 |0.19980101e8 |1 | 76 | 77 | #### Filter 78 | 79 | ```ruby 80 | scan = Dynamodb::Api.scan 81 | scan.from('cars'). 82 | filter('model = :model', ':model': 'S2000') 83 | items = scan.all.items 84 | ``` 85 | 86 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 87 | |:---|:---|:---|:---|:---| 88 | |4 |1 |S2000 |0.19980101e8 |1 | 89 | 90 | #### Limit 91 | 92 | ```ruby 93 | scan = Dynamodb::Api.scan 94 | scan.from('cars'). 95 | limit(1) 96 | items = scan.all.items 97 | ``` 98 | 99 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 100 | |:---|:---|:---|:---|:---| 101 | |1 |1 |Accord |0.19760508e8 |0 | 102 | 103 | #### Next(Paging) 104 | 105 | ```ruby 106 | scan = Dynamodb::Api.scan 107 | scan.from('cars'). 108 | limit(1) 109 | _items = scan.all.items 110 | items = scan.next.items 111 | ``` 112 | 113 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 114 | |:---|:---|:---|:---|:---| 115 | |2 |2 |CROWN |0.19550101e8 |0 | 116 | 117 | 118 | ### Query 119 | https://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method 120 | 121 | #### Partition(Hash) key 122 | 123 | ```ruby 124 | query = Dynamodb::Api.query 125 | query.from('cars').index('index_maker_id_release_date'). 126 | where(['maker_id', '=', 1]) 127 | items = query.all.items 128 | ``` 129 | 130 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 131 | |:---|:---|:---|:---|:---| 132 | |4 |1 |S2000 |0.19980101e8 |1 | 133 | |1 |1 |Accord |0.19760508e8 |0 | 134 | 135 | #### Partition key and Sort(Range) key 136 | 137 | ```ruby 138 | query = Dynamodb::Api.query 139 | query.from('cars').index('index_maker_id_release_date'). 140 | where([['maker_id', '=', 1], ['release_date', '>=', 19_980_101]]) 141 | items = query.all.items 142 | ``` 143 | 144 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 145 | |:---|:---|:---|:---|:---| 146 | |4 |1 |S2000 |0.19980101e8 |1 | 147 | 148 | #### Sorting 149 | 150 | ```ruby 151 | query = Dynamodb::Api.query 152 | query.from('cars').index('index_maker_id_release_date'). 153 | where(['maker_id', '=', 1]). 154 | order('asc') # default: 'desc' 155 | items = query.all.items 156 | ``` 157 | 158 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 159 | |:---|:---|:---|:---|:---| 160 | |1 |1 |Accord |0.19760508e8 |0 | 161 | |4 |1 |S2000 |0.19980101e8 |1 | 162 | 163 | #### Filter 164 | 165 | ```ruby 166 | query = Dynamodb::Api.query 167 | query.from('cars').index('index_maker_id_release_date'). 168 | where(['maker_id', '=', 1]). 169 | filter('model = :model', ':model': 'S2000') 170 | items = query.all.items 171 | ``` 172 | 173 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 174 | |:---|:---|:---|:---|:---| 175 | |4 |1 |S2000 |0.19980101e8 |1 | 176 | 177 | #### Limit 178 | 179 | ```ruby 180 | query = Dynamodb::Api.query 181 | query.from('cars').index('index_maker_id_release_date'). 182 | where(['maker_id', '=', 1]). 183 | order('asc'). # default: 'desc' 184 | limit(1) 185 | items = query.all.items 186 | ``` 187 | 188 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 189 | |:---|:---|:---|:---|:---| 190 | |1 |1 |Accord |0.19760508e8 |0 | 191 | 192 | #### Next(Paging) 193 | 194 | ```ruby 195 | query = Dynamodb::Api.query 196 | query.from('cars').index('index_maker_id_release_date'). 197 | where(['maker_id', '=', 1]). 198 | order('asc'). # default: 'desc' 199 | limit(1) 200 | _items = query.all.items 201 | items = query.next.items 202 | ``` 203 | 204 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 205 | |:---|:---|:---|:---|:---| 206 | |4 |1 |S2000 |0.19980101e8 |1 | 207 | 208 | #### Expression Attribute Names 209 | 210 | Words reserved in DynamoDB can not be used without special processing. 211 | In `dynamodb-api` , you can omit the special processing by putting `#` at the beginning of the word. 212 | Refer to the list of reserved words from [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html). 213 | 214 | ```ruby 215 | query = Dynamodb::Api.query 216 | query.from('cars').index('index_maker_id_release_date'). 217 | where(['maker_id', '=', 1]). 218 | filter('#status = :status', ':status': 1) 219 | items = query.all.items 220 | ``` 221 | 222 | | id | maker_id(Partition key) | model | release_date(Sort key) | status | 223 | |:---|:---|:---|:---|:---| 224 | |4 |1 |S2000 |0.19980101e8 |1 | 225 | 226 | If you don't add `#` to a reserved word, the following error will occur: 227 | 228 | Aws::DynamoDB::Errors::ValidationException: 229 | Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: [reserved word] 230 | 231 | ### Insert 232 | 233 | ```ruby 234 | item = { id: '5', maker_id: 1, maker: 'Honda', model: 'NSX', release_date: 19900914 } 235 | Dynamodb::Api.insert('cars', item) 236 | ``` 237 | 238 | ### Update 239 | 240 | ```ruby 241 | key = { id: '5' } 242 | value = { new_col: 'new' } 243 | Dynamodb::Api.update('cars', key, value) 244 | ``` 245 | 246 | ### Delete 247 | 248 | ```ruby 249 | key = { id: '5' } 250 | Dynamodb::Api.delete('cars', key) 251 | ``` 252 | 253 | ### Remove attributes 254 | 255 | ```ruby 256 | key = { id: '3' } 257 | attributes = ['status'] 258 | Dynamodb::Api.remove_attributes('cars', key, attributes) 259 | 260 | query = Dynamodb::Api.query 261 | query.from('cars').index('index_maker_id_release_date'). 262 | where(['maker_id', '=', 3]) 263 | items = query.all.items 264 | ``` 265 | 266 | | id | maker_id(Partition key) | model | release_date(Sort key) | 267 | |:---|:---|:---|:---| 268 | |3 |3 |Model S |0.20120601e8 | 269 | 270 | ### Other API operations 271 | 272 | `client` returns the `` . 273 | So, you can use all [API operations](https://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html). 274 | 275 | ```ruby 276 | client = Dynamodb::Api::Adapter.client # 277 | 278 | # e.g. 279 | client.create_backup( 280 | table_name: 'TableName', # required 281 | backup_name: 'BackupName', # required 282 | ) 283 | ``` 284 | 285 | ## Development 286 | 287 | - Run `docker-compose up` to run the dynamodb_local. 288 | - Run `docker-compose run ruby bundle exec rake spec` to run the tests. Or run `docker-compose run ruby bundle exec appraisal aws-sdk-* rake spec` to run the tests. Replase `*` with aws sdk major version. 289 | - You can also run `docker-compose run ruby bin/console` for an interactive prompt that will allow you to experiment. 290 | 291 | To install this gem onto your local machine, run `docker-compose run ruby bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `docker-compose run ruby bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 292 | 293 | ## Contributing 294 | 295 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dynamodb-api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 296 | 297 | ## License 298 | 299 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 300 | 301 | ## Code of Conduct 302 | 303 | Everyone interacting in the Dynamodb::Api project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/dynamodb-api/blob/master/CODE_OF_CONDUCT.md). 304 | --------------------------------------------------------------------------------