rom-dynamo 0.1.4 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MzVjNmE0OWM2ODc1N2ZkNDM4ZDAzYjliNWYzODE0MDZmZWUxYzFkOA==
5
- data.tar.gz: !binary |-
6
- ZTBlOWU4ZGY1NGZiYjM1MmI3MDg1NjMxYmE0NWE5NjViODUxY2JlNw==
2
+ SHA256:
3
+ metadata.gz: bb3d1008f7f0331e2ea2d49690cf9b95388c96143ca86718204e0fc53f3e8c4e
4
+ data.tar.gz: f45c64b787dab54ea6c821e196c3d3ab4a1aa181065a5338e5892156e606885c
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MDBkZmMwNzcyM2VlODE1NzQ1Y2ExOTgzNDUwMjhkNDA3ZmYzMGM4YzYxYWFl
10
- ODg1ZDlkYWEwNGEzODg2Mzk2YjFjMjI0MjUyYTAyODRiNTg3YjQyYjIxNWQ0
11
- ZGQ1ZjNmNTVhZDkxOWE4ZTk1NGNkMjFmNTE1MmFkYmUzMDliYTM=
12
- data.tar.gz: !binary |-
13
- ZGY0ZGRmOThiZDBhYzRiMzA2NjdjMDhhZWM4MzRiMGNhYWEwMzhmYzRiMjhk
14
- NDdlZWNhMzE2NjMyOTFkMTY4NWUyMDk4ZmU3YmUxM2I5MWI3NDdjNGU4Mzdk
15
- MTIxNjllNjc1ZTU3NjEyODBhZDc5YjljOGFkNzI1NjNlYTk5YzU=
6
+ metadata.gz: e595872d1620052b010b0b42905ae517d6a2461e1cc9b2a30e78ec661d6f89afa8e3bb4e9f2681b11a44b0372c8e9e6489c02864e768f3091fb539a41f5c0719
7
+ data.tar.gz: 81ec89f72f4e0b0b5a0be77b828c5db7bb730e6e4b118fca5a52d657d809396bcb63d494d592ebfaabf7d9d0b5ce618e8f323817059f6b537e4826ad1a0382c8
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
+ --require spec_helper
2
3
  --color
data/Gemfile CHANGED
@@ -5,5 +5,6 @@ gemspec
5
5
 
6
6
  # For development/testing
7
7
  gem 'dotenv'
8
- gem 'rom', :github => 'rom-rb/rom'
9
- gem 'virtus'
8
+ gem 'rom'
9
+ gem 'virtus'
10
+ gem 'rspec'
data/Rakefile CHANGED
@@ -1,2 +1,7 @@
1
+ #!/usr/bin/env rake
1
2
  require "bundler/gem_tasks"
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
2
5
 
6
+ task :default => :spec
7
+ RSpec::Core::RakeTask.new
@@ -5,21 +5,43 @@ module Rom
5
5
  module Commands
6
6
  # DynamoDB create command
7
7
  class Create < ROM::Commands::Create
8
- def execute(tuple)
9
- attributes = input[tuple]
10
- validator.call(attributes)
11
- relation.insert(attributes.to_h)
12
- []
8
+ def execute(tuples)
9
+ Array([tuples]).flatten.map do |tuple|
10
+ attributes = input[tuple]
11
+ dataset.insert(attributes.to_h)
12
+ end
13
+ end
14
+
15
+ def dataset
16
+ relation.dataset
17
+ end
18
+ end
19
+
20
+ # DynamoDB update command
21
+ class Update < ROM::Commands::Update
22
+ def execute(params)
23
+ attributes = input[params]
24
+ relation.map do |tuple|
25
+ dataset.update(tuple, attributes.to_h)
26
+ end
27
+ end
28
+
29
+ def dataset
30
+ relation.dataset
13
31
  end
14
32
  end
15
33
 
16
34
  # DynamoDB delete command
17
35
  class Delete < ROM::Commands::Delete
18
36
  def execute
19
- target.to_a.tap do |tuples|
20
- tuples.each { |t| relation.delete(t) }
37
+ relation.to_a.tap do |tuples|
38
+ tuples.each { |t| dataset.delete(t) }
21
39
  end
22
40
  end
41
+
42
+ def dataset
43
+ relation.dataset
44
+ end
23
45
  end
24
46
  end
25
47
  end
@@ -2,84 +2,108 @@ module Rom
2
2
  module Dynamo
3
3
  class Relation < ROM::Relation
4
4
  include Enumerable
5
- forward :restrict, :index_restrict
6
-
7
- def insert(*args)
8
- dataset.insert(*args)
9
- self
10
- end
11
-
12
- def delete(*args)
13
- dataset.delete(*args)
14
- self
15
- end
5
+ forward :restrict, :batch_restrict, :index_restrict
6
+ forward :limit, :reversed
7
+ adapter :dynamo
16
8
  end
17
9
 
18
10
  class Dataset
19
- include Equalizer.new(:name, :connection)
20
- attr_reader :name, :connection
11
+ include Enumerable
12
+ include Dry::Equalizer(:name, :connection)
13
+ extend Dry::Initializer[undefined: false]
14
+ EmptyQuery = { key_conditions: {}.freeze }.freeze
15
+
16
+ option :connection
17
+ option :name, proc(&:to_s)
18
+ option :table_keys, optional: true, reader: false
19
+ option :query, default: proc { EmptyQuery }, reader: false
21
20
  alias_method :ddb, :connection
22
21
 
23
- def initialize(name, ddb, conditions = nil)
24
- @name, @connection = name, ddb
25
- @conditions = conditions || {}
26
- end
27
-
28
22
  ############# READ #############
23
+
29
24
  def each(&block)
30
- each_item({
31
- consistent_read: true,
32
- key_conditions: @conditions
33
- }, &block)
25
+ block.nil? ? to_enum : begin
26
+ result = start_query(consistent_read: true)
27
+ result.each_page { |p| p[:items].each(&block) }
28
+ end
34
29
  end
35
30
 
36
31
  def restrict(query = nil)
37
32
  return self if query.nil?
38
- conds = query_to_conditions(query)
39
- conds = @conditions.merge(conds)
40
- dup_as(Dataset, conditions: conds)
33
+ dup_with_query(Dataset, query)
34
+ end
35
+
36
+ def batch_restrict(keys)
37
+ dup_as(BatchGetDataset, keys: keys.map do |k|
38
+ Hash[table_keys.zip(k.is_a?(Array) ? k : [k])]
39
+ end)
41
40
  end
42
41
 
43
42
  def index_restrict(index, query)
44
- conds = query_to_conditions(query)
45
- conds = @conditions.merge(conds)
46
- dup_as(GlobalIndexDataset, index: index, conditions: conds)
43
+ dup_with_query(GlobalIndexDataset, query, index_name: index.to_s)
44
+ end
45
+
46
+ ############# PAGINATION #############
47
+
48
+ def limit(limit)
49
+ dup_with_query(self.class, nil, limit: limit.to_i)
50
+ end
51
+
52
+ def reversed
53
+ dup_with_query(self.class, nil, scan_index_forward: false)
47
54
  end
48
55
 
49
56
  ############# WRITE #############
50
57
  def insert(hash)
51
- connection.put_item({
52
- table_name: name,
53
- item: hash
54
- })
58
+ opts = { table_name: name, item: stringify_keys(hash) }
59
+ connection.put_item(opts).attributes
55
60
  end
56
61
 
57
62
  def delete(hash)
63
+ hash = stringify_keys(hash)
58
64
  connection.delete_item({
59
65
  table_name: name,
60
66
  key: hash_to_key(hash),
61
67
  expected: to_expected(hash),
62
- })
68
+ }).attributes
69
+ end
70
+
71
+ def update(keys, hash)
72
+ connection.update_item({
73
+ table_name: name, key: hash_to_key(stringify_keys(keys)),
74
+ attribute_updates: hash.each_with_object({}) do |(k, v), out|
75
+ out[k] = { value: dump_value(v), action: 'PUT' } if !keys[k]
76
+ end
77
+ }).attributes
63
78
  end
64
79
 
65
80
  ############# HELPERS #############
66
81
  private
67
- def each_item(options, &block)
68
- puts "Querying #{name} ...\nWith: #{options.inspect}"
69
- connection.query(options.merge({
70
- table_name: name
71
- })).each_page do |page|
72
- page[:items].each(&block)
82
+ def batch_get_each_item(keys, &block)
83
+ !keys.empty? && ddb.batch_get_item({
84
+ request_items: { name => { keys: keys } },
85
+ }).each_page do |page|
86
+ out = page[:responses][name]
87
+ out.each(&block)
73
88
  end
74
89
  end
75
90
 
76
- def query_to_conditions(query)
77
- Hash[query.map do |key, value|
78
- [key, {
79
- attribute_value_list: [value],
80
- comparison_operator: "EQ"
81
- }]
82
- end]
91
+ def dup_with_query(klass, key_hash, opts = {})
92
+ opts = @query.merge(opts)
93
+
94
+ if key_hash && !key_hash.empty?
95
+ conditions = @query[:key_conditions]
96
+ opts[:key_conditions] = conditions.merge(Hash[
97
+ key_hash.map do |key, value|
98
+ [key, {
99
+ attribute_value_list: [value],
100
+ comparison_operator: "EQ"
101
+ }]
102
+ end
103
+ ]).freeze
104
+ end
105
+
106
+ dup_as(klass, query: opts.freeze)
83
107
  end
84
108
 
85
109
  def to_expected(hash)
@@ -96,45 +120,63 @@ module Rom
96
120
 
97
121
  def table_keys
98
122
  @table_keys ||= begin
99
- resp = ddb.describe_table(table_name: name)
100
- keys = resp.first[:table][:key_schema]
101
- keys.map(&:attribute_name)
123
+ r = ddb.describe_table(table_name: name)
124
+ r[:table][:key_schema].map(&:attribute_name)
102
125
  end
103
126
  end
104
127
 
128
+ def start_query(opts = {}, &block)
129
+ opts = @query.merge(table_name: name).merge!(opts)
130
+ puts "Querying DDB: #{opts.inspect}"
131
+ ddb.query(opts)
132
+ end
133
+
105
134
  def dup_as(klass, opts = {})
106
135
  table_keys # To populate keys once at top-level Dataset
107
- vars = [:@name, :@connection, :@conditions, :@table_keys]
108
- klass.allocate.tap do |out|
109
- vars.each { |k| out.instance_variable_set(k, instance_variable_get(k)) }
110
- opts.each { |k, v| out.instance_variable_set("@#{k}", v) }
111
- end
136
+ attrs = Dataset.dry_initializer.attributes(self)
137
+ klass.new(attrs.merge(opts))
138
+ end
139
+
140
+ # String modifiers
141
+ def stringify_keys(hash)
142
+ hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
143
+ end
144
+
145
+ def dump_value(v)
146
+ return v.new_offset(0).iso8601(6) if v.is_a?(DateTime)
147
+ v.is_a?(Time) ? v.utc.iso8601(6) : v
112
148
  end
113
149
  end
114
150
 
115
- # Dataset queried via a Global Index
116
- class GlobalIndexDataset < Dataset
117
- attr_accessor :index
151
+ # Batch get using an array of key queries
152
+ # [{ key => val }, { key => val }, ...]
153
+ class BatchGetDataset < Dataset
154
+ option :keys
118
155
 
156
+ # Query for records
119
157
  def each(&block)
120
- # Pull record IDs from Global Index
121
- keys = []; each_item({
122
- key_conditions: @conditions,
123
- index_name: @index
124
- }) { |hash| keys << hash_to_key(hash) }
125
-
126
- # Bail if we have nothing
127
- return if keys.empty?
158
+ batch_get_each_item(@keys, &block)
159
+ end
160
+ end
128
161
 
129
- # Query for the actual records
130
- ddb.batch_get_item({
131
- request_items: { name => { keys: keys } },
132
- }).each_page do |page|
133
- out = page[:responses][name]
134
- out.each(&block)
162
+ # Dataset queried via a Global Secondary Index
163
+ # Paginate through keys from Global Index and
164
+ # call BatchGetItem for keys from each page
165
+ class GlobalIndexDataset < Dataset
166
+ def each(&block)
167
+ if @query[:limit]
168
+ each_item(start_query, &block)
169
+ else
170
+ result = start_query(limit: 100)
171
+ result.each_page { |p| each_item(p, &block) }
135
172
  end
136
173
  end
137
174
 
175
+ private def each_item(result, &block)
176
+ keys = result[:items].map { |h| hash_to_key(h) }
177
+ batch_get_each_item(keys, &block)
178
+ end
138
179
  end
180
+
139
181
  end
140
182
  end
@@ -1,12 +1,19 @@
1
- require 'uri'
2
- require 'rom/repository'
1
+ require 'addressable/uri'
2
+ require 'rom/gateway'
3
3
 
4
4
  module Rom
5
5
  module Dynamo
6
- class Repository < ROM::Repository
6
+ class Gateway < ROM::Gateway
7
+ attr_reader :ddb, :options
8
+
7
9
  def initialize(uri)
8
- uri = URI.parse(uri)
9
- @connection = Aws::DynamoDB::Client.new(region: uri.host)
10
+ uri = Addressable::URI.parse(uri)
11
+ opts = { region: uri.host }
12
+ opts.merge!(uri.query_values) if uri.query
13
+ opts.keys.each { |k| opts[k.to_sym] = opts.delete(k) }
14
+
15
+ @options = opts
16
+ @ddb = Aws::DynamoDB::Client.new(@options)
10
17
  @prefix = uri.path.gsub('/', '')
11
18
  @datasets = {}
12
19
  end
@@ -17,17 +24,22 @@ module Rom
17
24
 
18
25
  def dataset(name)
19
26
  name = "#{@prefix}#{name}"
20
- @datasets[name] ||= Dataset.new(name, @connection)
27
+ @datasets[name] ||= _has?(name) && Dataset.new(connection: @ddb, name: name)
21
28
  end
22
29
 
23
30
  def dataset?(name)
24
- name = "#{@prefix}#{name}"
25
- list = connection.list_tables
26
- list[:table_names].include?(name)
31
+ !!self[name]
27
32
  end
28
33
 
29
34
  def [](name)
30
- @datasets[name]
35
+ @datasets["#{@prefix}#{name}"]
36
+ end
37
+
38
+ private
39
+ def _has?(name)
40
+ @ddb.describe_table(table_name: name)
41
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
42
+ return false
31
43
  end
32
44
  end
33
45
  end
@@ -1,5 +1,5 @@
1
1
  module Rom
2
2
  module Dynamo
3
- VERSION = "0.1.4"
3
+ VERSION = "0.14.0"
4
4
  end
5
5
  end
data/lib/rom/dynamo.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'rom'
2
- require 'aws-sdk-core'
2
+ require 'date'
3
+ require 'aws-sdk-dynamodb'
3
4
  require "rom/dynamo/version"
4
5
  require 'rom/dynamo/relation'
5
6
  require 'rom/dynamo/commands'
6
7
  require 'rom/dynamo/repository'
7
8
 
9
+ # jRuby HACK: https://github.com/jruby/jruby/issues/3645#issuecomment-181660161
10
+ module Aws; const_set(:DynamoDB, Aws::DynamoDB) end
11
+
12
+ # Register adapter with ROM-rb
8
13
  ROM.register_adapter(:dynamo, Rom::Dynamo)
data/lib/rom-dynamo.rb CHANGED
@@ -1 +1,2 @@
1
1
  require 'rom/dynamo'
2
+ ROM::Dynamo = Rom::Dynamo
data/rom-dynamo.gemspec CHANGED
@@ -24,10 +24,12 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ["lib"]
25
25
 
26
26
  # Runtime
27
- spec.add_runtime_dependency "rom", "~> 0.5"
28
- spec.add_runtime_dependency "aws-sdk-core"
27
+ spec.add_runtime_dependency "addressable", "~> 2.3"
28
+ spec.add_runtime_dependency "rom", ">= 1.0", "< 6.0"
29
+ spec.add_runtime_dependency "aws-sdk-dynamodb", "~> 1.0"
29
30
 
30
31
  # Development
31
- spec.add_development_dependency "bundler", "~> 1.8"
32
- spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "activesupport", ">= 4.0", "< 7.0"
33
+ spec.add_development_dependency "bundler", "~> 1.7"
34
+ spec.add_development_dependency "rake", "~> 13.0"
33
35
  end
metadata CHANGED
@@ -1,71 +1,111 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-dynamo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Rykov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-02-19 00:00:00.000000000 Z
11
+ date: 2020-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.3'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rom
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - ~>
31
+ - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: '0.5'
33
+ version: '1.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '6.0'
20
37
  type: :runtime
21
38
  prerelease: false
22
39
  version_requirements: !ruby/object:Gem::Requirement
23
40
  requirements:
24
- - - ~>
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '1.0'
44
+ - - "<"
25
45
  - !ruby/object:Gem::Version
26
- version: '0.5'
46
+ version: '6.0'
27
47
  - !ruby/object:Gem::Dependency
28
- name: aws-sdk-core
48
+ name: aws-sdk-dynamodb
29
49
  requirement: !ruby/object:Gem::Requirement
30
50
  requirements:
31
- - - ! '>='
51
+ - - "~>"
32
52
  - !ruby/object:Gem::Version
33
- version: '0'
53
+ version: '1.0'
34
54
  type: :runtime
35
55
  prerelease: false
36
56
  version_requirements: !ruby/object:Gem::Requirement
37
57
  requirements:
38
- - - ! '>='
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: activesupport
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '4.0'
68
+ - - "<"
69
+ - !ruby/object:Gem::Version
70
+ version: '7.0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '4.0'
78
+ - - "<"
39
79
  - !ruby/object:Gem::Version
40
- version: '0'
80
+ version: '7.0'
41
81
  - !ruby/object:Gem::Dependency
42
82
  name: bundler
43
83
  requirement: !ruby/object:Gem::Requirement
44
84
  requirements:
45
- - - ~>
85
+ - - "~>"
46
86
  - !ruby/object:Gem::Version
47
- version: '1.8'
87
+ version: '1.7'
48
88
  type: :development
49
89
  prerelease: false
50
90
  version_requirements: !ruby/object:Gem::Requirement
51
91
  requirements:
52
- - - ~>
92
+ - - "~>"
53
93
  - !ruby/object:Gem::Version
54
- version: '1.8'
94
+ version: '1.7'
55
95
  - !ruby/object:Gem::Dependency
56
96
  name: rake
57
97
  requirement: !ruby/object:Gem::Requirement
58
98
  requirements:
59
- - - ~>
99
+ - - "~>"
60
100
  - !ruby/object:Gem::Version
61
- version: '10.0'
101
+ version: '13.0'
62
102
  type: :development
63
103
  prerelease: false
64
104
  version_requirements: !ruby/object:Gem::Requirement
65
105
  requirements:
66
- - - ~>
106
+ - - "~>"
67
107
  - !ruby/object:Gem::Version
68
- version: '10.0'
108
+ version: '13.0'
69
109
  description: DynamoDB adapter for Ruby Object Mapper
70
110
  email:
71
111
  - mrykov@gmail.com
@@ -73,9 +113,9 @@ executables: []
73
113
  extensions: []
74
114
  extra_rdoc_files: []
75
115
  files:
76
- - .gitignore
77
- - .rspec
78
- - .travis.yml
116
+ - ".gitignore"
117
+ - ".rspec"
118
+ - ".travis.yml"
79
119
  - Gemfile
80
120
  - LICENSE.txt
81
121
  - README.md
@@ -99,17 +139,17 @@ require_paths:
99
139
  - lib
100
140
  required_ruby_version: !ruby/object:Gem::Requirement
101
141
  requirements:
102
- - - ! '>='
142
+ - - ">="
103
143
  - !ruby/object:Gem::Version
104
144
  version: '0'
105
145
  required_rubygems_version: !ruby/object:Gem::Requirement
106
146
  requirements:
107
- - - ! '>='
147
+ - - ">="
108
148
  - !ruby/object:Gem::Version
109
149
  version: '0'
110
150
  requirements: []
111
151
  rubyforge_project:
112
- rubygems_version: 2.4.5
152
+ rubygems_version: 2.7.8
113
153
  signing_key:
114
154
  specification_version: 4
115
155
  summary: DynamoDB adapter for Ruby Object Mapper