rom-dynamo 0.1.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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