dynamodb_framework 1.3.0 → 1.4.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dynamodb_framework/dynamodb_index.rb +119 -0
  3. data/lib/dynamodb_framework/dynamodb_query.rb +111 -0
  4. data/lib/dynamodb_framework/dynamodb_repository.rb +37 -7
  5. data/lib/dynamodb_framework/dynamodb_table.rb +114 -0
  6. data/lib/dynamodb_framework/version.rb +1 -1
  7. data/lib/dynamodb_framework.rb +20 -0
  8. data/spec/dynamodb_index_spec.rb +212 -0
  9. data/spec/dynamodb_migration_manager_spec.rb +134 -0
  10. data/spec/dynamodb_namespace_migration_manager_spec.rb +134 -0
  11. data/spec/dynamodb_query_spec.rb +87 -0
  12. data/spec/dynamodb_repository_spec.rb +306 -0
  13. data/spec/dynamodb_table_manager_spec.rb +156 -0
  14. data/spec/dynamodb_table_spec.rb +245 -0
  15. data/spec/example_index.rb +45 -0
  16. data/spec/example_table.rb +41 -0
  17. data/spec/hash_helper_spec.rb +129 -0
  18. data/spec/spec_helper.rb +34 -0
  19. data/spec/test_item.rb +6 -0
  20. data/spec/test_migration_script1.rb +24 -0
  21. data/spec/test_migration_script2.rb +24 -0
  22. metadata +35 -25
  23. data/.gitignore +0 -14
  24. data/.idea/.name +0 -1
  25. data/.idea/.rakeTasks +0 -7
  26. data/.idea/encodings.xml +0 -6
  27. data/.idea/misc.xml +0 -33
  28. data/.idea/modules.xml +0 -8
  29. data/.idea/vcs.xml +0 -6
  30. data/.rspec +0 -3
  31. data/CODE_OF_CONDUCT.md +0 -49
  32. data/Gemfile +0 -12
  33. data/LICENSE.txt +0 -21
  34. data/README.md +0 -394
  35. data/Rakefile +0 -2
  36. data/dynamodb_framework.gemspec +0 -29
  37. data/script/cleanup.sh +0 -6
  38. data/script/container_loop.sh +0 -6
  39. data/script/docker-compose.yml +0 -5
  40. data/script/restart.sh +0 -3
  41. data/script/start.sh +0 -4
  42. data/script/stop.sh +0 -2
  43. data/yard.sh +0 -3
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe DynamoDbFramework::MigrationManager do
4
+
5
+ let(:store) do
6
+ DynamoDbFramework::Store.new({ endpoint: DYNAMODB_STORE_ENDPOINT, aws_region: 'eu-west-1' })
7
+ end
8
+
9
+ let(:table_manager) do
10
+ DynamoDbFramework::TableManager.new(store)
11
+ end
12
+
13
+ let(:migration_table_name) do
14
+ 'dynamodb_framework_migrations'
15
+ end
16
+
17
+ let(:repository) do
18
+ repository = DynamoDbFramework::Repository.new(store)
19
+ repository.table_name = migration_table_name
20
+ repository
21
+ end
22
+
23
+ subject do
24
+ DynamoDbFramework::MigrationManager.new(store)
25
+ end
26
+
27
+ context '#connect' do
28
+
29
+ it 'should create the migration table when not found' do
30
+
31
+ table_manager.drop(migration_table_name)
32
+
33
+ subject.connect()
34
+
35
+ table_manager.exists?(migration_table_name)
36
+
37
+ end
38
+
39
+ it 'should connect when migration table already exists' do
40
+
41
+ if !table_manager.exists?(migration_table_name)
42
+ builder = DynamoDbFramework::AttributesBuilder.new
43
+ builder.add(:id, :S)
44
+ table_manager.create(migration_table_name, builder.attributes, :id)
45
+ end
46
+
47
+ subject.connect()
48
+
49
+ table_manager.drop(migration_table_name)
50
+
51
+ end
52
+
53
+ end
54
+
55
+ context '#apply' do
56
+
57
+ it 'should apply all migration scripts when none have been applied previously' do
58
+
59
+ table_manager.drop(migration_table_name)
60
+
61
+ subject.connect()
62
+
63
+ subject.apply()
64
+
65
+ expect(table_manager.exists?('test1')).to eq(true)
66
+ expect(table_manager.exists?('test2')).to eq(true)
67
+
68
+ records = repository.all()
69
+ expect(records.length).to eq(2)
70
+ expect(records[0]['timestamp']).to eq('20160318110710')
71
+ expect(records[1]['timestamp']).to eq('20160318110730')
72
+
73
+ end
74
+
75
+ it 'should only apply migration scripts that have not been previously applied' do
76
+
77
+ table_manager.drop(migration_table_name)
78
+
79
+ subject.connect()
80
+
81
+ #setup migration history to have already previously applied script 1
82
+ script1 = TestMigrationScript1.new
83
+ script1.apply()
84
+
85
+ expect(table_manager.exists?('test1')).to eq(true)
86
+ repository.put({ :timestamp => script1.timestamp })
87
+ records = repository.all()
88
+ expect(records.length).to eq(1)
89
+ expect(records[0]['timestamp']).to eq('20160318110710')
90
+
91
+ #run migration apply
92
+ subject.apply()
93
+
94
+ #verify that script2 has been ran
95
+ records = repository.all()
96
+ expect(records.length).to eq(2)
97
+ expect(records[1]['timestamp']).to eq('20160318110730')
98
+
99
+ end
100
+
101
+ end
102
+
103
+ context '#rollback' do
104
+
105
+ it 'should roll back the last migration script that was applied' do
106
+
107
+ table_manager.drop(migration_table_name)
108
+ subject.connect()
109
+
110
+ #setup migration history to have already previously applied script 1
111
+ script1 = TestMigrationScript1.new
112
+ script1.apply()
113
+
114
+ expect(table_manager.exists?('test1')).to eq(true)
115
+ repository.put({ :timestamp => script1.timestamp })
116
+ records = repository.all()
117
+ expect(records.length).to eq(1)
118
+ expect(records[0]['timestamp']).to eq('20160318110710')
119
+
120
+ #execute a rollback
121
+ subject.rollback()
122
+
123
+ #verify that the test1 table has been removed as part of the rollback
124
+ expect(table_manager.exists?('test1')).to eq(false)
125
+
126
+ end
127
+
128
+ end
129
+
130
+ after do
131
+ table_manager.drop(migration_table_name)
132
+ end
133
+
134
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe DynamoDbFramework::Namespace::MigrationManager do
4
+
5
+ let(:store) do
6
+ DynamoDbFramework::Store.new({ endpoint: DYNAMODB_STORE_ENDPOINT, aws_region: 'eu-west-1' })
7
+ end
8
+
9
+ let(:table_manager) do
10
+ DynamoDbFramework::TableManager.new(store)
11
+ end
12
+
13
+ let(:migration_table_name) do
14
+ 'dynamodb_framework_migration_history'
15
+ end
16
+
17
+ let(:repository) do
18
+ repository = DynamoDbFramework::Repository.new(store)
19
+ repository.table_name = migration_table_name
20
+ repository
21
+ end
22
+
23
+ subject do
24
+ DynamoDbFramework::Namespace::MigrationManager.new(store)
25
+ end
26
+
27
+ context '#connect' do
28
+
29
+ it 'should create the migration table when not found' do
30
+
31
+ table_manager.drop(migration_table_name)
32
+
33
+ subject.connect()
34
+
35
+ table_manager.exists?(migration_table_name)
36
+
37
+ end
38
+
39
+ it 'should connect when migration table already exists' do
40
+
41
+ if !table_manager.exists?(migration_table_name)
42
+ builder = DynamoDbFramework::AttributesBuilder.new
43
+ builder.add(:id, :S)
44
+ table_manager.create(migration_table_name, builder.attributes, :id)
45
+ end
46
+
47
+ subject.connect()
48
+
49
+ table_manager.drop(migration_table_name)
50
+
51
+ end
52
+
53
+ end
54
+
55
+ context '#apply' do
56
+
57
+ it 'should apply all migration scripts when none have been applied previously' do
58
+
59
+ table_manager.drop(migration_table_name)
60
+
61
+ subject.connect()
62
+
63
+ subject.apply('test_namespace')
64
+
65
+ expect(table_manager.exists?('test1')).to eq(true)
66
+ expect(table_manager.exists?('test2')).to eq(true)
67
+
68
+ records = repository.all()
69
+ expect(records.length).to eq(2)
70
+ expect(records[0]['timestamp']).to eq('20160318110710')
71
+ expect(records[1]['timestamp']).to eq('20160318110730')
72
+
73
+ end
74
+
75
+ it 'should only apply migration scripts that have not been previously applied' do
76
+
77
+ table_manager.drop(migration_table_name)
78
+
79
+ subject.connect()
80
+
81
+ #setup migration history to have already previously applied script 1
82
+ script1 = TestMigrationScript1.new
83
+ script1.apply()
84
+
85
+ expect(table_manager.exists?('test1')).to eq(true)
86
+ repository.put({ :timestamp => script1.timestamp, :namespace => script1.namespace })
87
+ records = repository.all()
88
+ expect(records.length).to eq(1)
89
+ expect(records[0]['timestamp']).to eq('20160318110710')
90
+
91
+ #run migration apply
92
+ subject.apply('test_namespace')
93
+
94
+ #verify that script2 has been ran
95
+ records = repository.all()
96
+ expect(records.length).to eq(2)
97
+ expect(records[1]['timestamp']).to eq('20160318110730')
98
+
99
+ end
100
+
101
+ end
102
+
103
+ context '#rollback' do
104
+
105
+ it 'should roll back the last migration script that was applied' do
106
+
107
+ table_manager.drop(migration_table_name)
108
+ subject.connect()
109
+
110
+ #setup migration history to have already previously applied script 1
111
+ script1 = TestMigrationScript1.new
112
+ script1.apply()
113
+
114
+ expect(table_manager.exists?('test1')).to eq(true)
115
+ repository.put({ :timestamp => script1.timestamp, :namespace => script1.namespace })
116
+ records = repository.all()
117
+ expect(records.length).to eq(1)
118
+ expect(records[0]['timestamp']).to eq('20160318110710')
119
+
120
+ #execute a rollback
121
+ subject.rollback('test_namespace')
122
+
123
+ #verify that the test1 table has been removed as part of the rollback
124
+ expect(table_manager.exists?('test1')).to eq(false)
125
+
126
+ end
127
+
128
+ end
129
+
130
+ after do
131
+ table_manager.drop(migration_table_name)
132
+ end
133
+
134
+ end
@@ -0,0 +1,87 @@
1
+ RSpec.describe DynamoDbFramework::Query do
2
+
3
+ let(:store) do
4
+ DynamoDbFramework::Store.new({ endpoint: DYNAMODB_STORE_ENDPOINT, aws_region: 'eu-west-1' })
5
+ end
6
+
7
+ let(:repository) do
8
+ DynamoDbFramework::Repository.new(store)
9
+ end
10
+
11
+ let(:table_manager) do
12
+ DynamoDbFramework::TableManager.new(store)
13
+ end
14
+
15
+ let(:table_name) { ExampleTable2.config[:table_name] }
16
+
17
+ def create_query_item(name, number)
18
+ item = TestItem.new
19
+ item.id = SecureRandom.uuid
20
+ item.name = name
21
+ item.timestamp = Time.now
22
+ item.number = number
23
+ repository.table_name = table_name
24
+ repository.put(item)
25
+ end
26
+
27
+ before do
28
+ table_manager.drop(table_name)
29
+ ExampleTable2.create(store: store)
30
+
31
+ create_query_item('name 1', 1)
32
+ create_query_item('name 1', 2)
33
+ create_query_item('name 1', 3)
34
+ create_query_item('name 1', 4)
35
+ create_query_item('name 2', 1)
36
+ create_query_item('name 2', 2)
37
+ create_query_item('name 2', 3)
38
+ create_query_item('name 3', 1)
39
+ create_query_item('name 3', 2)
40
+ end
41
+
42
+ describe '#build' do
43
+ it 'should correctly generate the expression string and params' do
44
+ string,params = DynamoDbFramework::Query.new(table_name: table_name, partition_key: :name, partition_value: 'name 1')
45
+ .number.gt_eq(1)
46
+ .and
47
+ .number.lt_eq(5)
48
+ .build
49
+ expect(string).to eq '#number >= :p0 and #number <= :p1'
50
+ expect(params['#number']).to eq 'number'
51
+ expect(params[':p0']).to eq 1
52
+ expect(params[':p1']).to eq 5
53
+ end
54
+ end
55
+
56
+ describe '#execute' do
57
+ it 'should return the expected items' do
58
+ results = DynamoDbFramework::Query.new(table_name: table_name, partition_key: :name, partition_value: 'name 1')
59
+ .number.gt_eq(1)
60
+ .and
61
+ .number.lt_eq(5)
62
+ .execute(store: store)
63
+ expect(results.length).to eq 4
64
+ end
65
+ context 'when limit is specified' do
66
+ it 'should return the expected items' do
67
+ results = DynamoDbFramework::Query.new(table_name: table_name, partition_key: :name, partition_value: 'name 1')
68
+ .number.gt_eq(1)
69
+ .and
70
+ .number.lt_eq(5)
71
+ .execute(store: store, limit: 1)
72
+ expect(results.length).to eq 1
73
+ end
74
+ end
75
+ context 'when count is specified' do
76
+ it 'should return the expected count' do
77
+ count = DynamoDbFramework::Query.new(table_name: table_name, partition_key: :name, partition_value: 'name 1')
78
+ .number.gt_eq(1)
79
+ .and
80
+ .number.lt_eq(5)
81
+ .execute(store: store, count: 4)
82
+ expect(count).to eq 4
83
+ end
84
+ end
85
+ end
86
+
87
+ end
@@ -0,0 +1,306 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe DynamoDbFramework::Repository do
4
+ let(:store) do
5
+ DynamoDbFramework::Store.new({ endpoint: DYNAMODB_STORE_ENDPOINT, aws_region: 'eu-west-1' })
6
+ end
7
+
8
+ let(:schema_manager) do
9
+ DynamoDbFramework::TableManager.new(store)
10
+ end
11
+
12
+ subject do
13
+ DynamoDbFramework::Repository.new(store)
14
+ end
15
+
16
+ def create_query_item(name, number, table_name)
17
+ item = TestItem.new
18
+ item.id = SecureRandom.uuid
19
+ item.name = name
20
+ item.timestamp = Time.now
21
+ item.number = number
22
+ subject.table_name = table_name
23
+ subject.put(item)
24
+ end
25
+
26
+ describe '#put' do
27
+ let(:current_time) { Time.now }
28
+ let(:id ) { 'Item1' }
29
+ let(:name ) { 'Name' }
30
+ let(:timestamp ) { current_time }
31
+ let(:number ) { 5 }
32
+
33
+ let(:item) do
34
+ TestItem.new.tap do |obj|
35
+ obj.id = id
36
+ obj.name = name
37
+ obj.timestamp = timestamp
38
+ obj.number = number
39
+ end
40
+ end
41
+
42
+ before do
43
+ schema_manager.drop('put')
44
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
45
+ attributes_builder.add(:id, :S)
46
+ schema_manager.create('put', attributes_builder.attributes, :id)
47
+ subject.table_name = 'put'
48
+ end
49
+
50
+ it 'can store an item in a table' do
51
+ expect { subject.put(item) }.to_not raise_error
52
+ end
53
+
54
+ context 'when one of the entity values is nil' do
55
+ let(:name) { nil }
56
+
57
+ it 'does not raise an error' do
58
+ expect { subject.put(item) }.to_not raise_error
59
+
60
+ stored_item = subject.get_by_key('id', id)
61
+
62
+ expect(stored_item['id']).to eq(id)
63
+ expect(stored_item['name']).to eq(name)
64
+ expect(stored_item['number']).to eq(number)
65
+ end
66
+ end
67
+
68
+ context 'when one of the entity values is an empty string' do
69
+ let(:name) { '' }
70
+
71
+ it 'does not raise an error' do
72
+ expect { subject.put(item) }.to_not raise_error
73
+
74
+ stored_item = subject.get_by_key('id', id)
75
+
76
+ expect(stored_item['id']).to eq(id)
77
+ expect(stored_item['name']).to eq(nil)
78
+ expect(stored_item['number']).to eq(number)
79
+ end
80
+ end
81
+
82
+ context 'when a date attribute is a DateTime class' do
83
+ let(:timestamp) {DateTime.now}
84
+
85
+ it 'stores the attribute in an iso8601 string format' do
86
+
87
+ subject.put(item)
88
+ stored_item = subject.get_by_key('id', id)
89
+
90
+ expect(stored_item['timestamp']).to eq(timestamp.iso8601)
91
+ end
92
+ end
93
+
94
+ context 'when a date attribute is a Time class' do
95
+ let(:timestamp) {Time.now}
96
+
97
+ it 'stores the attribute as an Epoch int' do
98
+ subject.put(item)
99
+ stored_item = subject.get_by_key('id', id)
100
+
101
+ expect(stored_item['timestamp']).to eq(timestamp.to_i)
102
+ end
103
+ end
104
+
105
+ after do
106
+ schema_manager.drop('put')
107
+ end
108
+
109
+ end
110
+
111
+ context '#delete' do
112
+ before do
113
+ schema_manager.drop('delete')
114
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
115
+ attributes_builder.add(:id, :S)
116
+ schema_manager.create('delete', attributes_builder.attributes, :id)
117
+ end
118
+
119
+ it 'can delete an item from a table' do
120
+ item = TestItem.new
121
+ item.id = 'Item1'
122
+ item.name = 'Name'
123
+ item.timestamp = Time.now.to_i
124
+ item.number = 5
125
+
126
+ subject.table_name = 'delete'
127
+ subject.put(item)
128
+
129
+ subject.delete({ id: item.id })
130
+ end
131
+
132
+ after do
133
+ schema_manager.drop('delete')
134
+ end
135
+
136
+ end
137
+
138
+ context '#get_by_key' do
139
+ before do
140
+ schema_manager.drop('get_by_key')
141
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
142
+ attributes_builder.add(:id, :S)
143
+ schema_manager.create('get_by_key', attributes_builder.attributes, :id)
144
+ end
145
+
146
+ it 'can get an item from a table by the item key' do
147
+ item = TestItem.new
148
+ item.id = 'Item1'
149
+ item.name = 'Name'
150
+ item.timestamp = Time.now.to_i
151
+ item.number = 5
152
+
153
+ subject.table_name = 'get_by_key'
154
+ subject.put(item)
155
+
156
+ item = subject.get_by_key('id', item.id)
157
+ expect(item).to_not be_nil
158
+ expect(item["id"]).to eq('Item1')
159
+ expect(item["name"]).to eq('Name')
160
+ end
161
+
162
+ after do
163
+ schema_manager.drop('get_by_key')
164
+ end
165
+
166
+ end
167
+
168
+ context '#query no index' do
169
+ before do
170
+ schema_manager.drop('query')
171
+
172
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
173
+ attributes_builder.add(:name, :S, :partition)
174
+ attributes_builder.add(:id, :S, :range)
175
+
176
+ schema_manager.create_table(name: 'query', attributes: attributes_builder.attributes)
177
+
178
+
179
+ create_query_item('name 1', 1, 'query')
180
+ create_query_item('name 1', 2, 'query')
181
+ create_query_item('name 1', 3, 'query')
182
+ create_query_item('name 1', 4, 'query')
183
+ create_query_item('name 2', 1, 'query')
184
+ create_query_item('name 2', 2, 'query')
185
+ create_query_item('name 2', 3, 'query')
186
+ create_query_item('name 3', 1, 'query')
187
+ create_query_item('name 3', 2, 'query')
188
+ end
189
+
190
+ it 'should return all items within a partition that match a filter expression' do
191
+ subject.table_name = 'query'
192
+
193
+ results = subject.query(:name, 'name 1', nil, nil, '#number > :number', { '#number' => 'number', ':number' => 2})
194
+
195
+ expect(results.length).to eq(2)
196
+ end
197
+
198
+ it 'should count all items within a partition that match a filter expression' do
199
+ subject.table_name = 'query'
200
+
201
+ count = subject.query(:name, 'name 1', nil, nil, '#number > :number', { '#number' => 'number', ':number' => 2}, nil, nil, true)
202
+
203
+ expect(count).to eq(2)
204
+ end
205
+
206
+ after do
207
+ schema_manager.drop('query')
208
+ end
209
+ end
210
+
211
+ context '#query index' do
212
+ before do
213
+ schema_manager.drop('query_index')
214
+
215
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
216
+ attributes_builder.add(:id, :S)
217
+ attributes_builder.add(:name, :S)
218
+
219
+ global_indexes = []
220
+ index1 = schema_manager.create_global_index('name_index', :name)
221
+ global_indexes.push(index1)
222
+ schema_manager.create('query_index', attributes_builder.attributes, :id, nil, 20, 10, global_indexes)
223
+
224
+
225
+ create_query_item('name 1', 1, 'query_index')
226
+ create_query_item('name 1', 2, 'query_index')
227
+ create_query_item('name 1', 3, 'query_index')
228
+ create_query_item('name 1', 4, 'query_index')
229
+ create_query_item('name 2', 1, 'query_index')
230
+ create_query_item('name 2', 2, 'query_index')
231
+ create_query_item('name 2', 3, 'query_index')
232
+ create_query_item('name 3', 1, 'query_index')
233
+ create_query_item('name 3', 2, 'query_index')
234
+ end
235
+
236
+ it 'should return all items within an index partition that match a filter expression' do
237
+ subject.table_name = 'query_index'
238
+
239
+ results = subject.query(:name, 'name 1', nil, nil, '#number > :number', { '#number' => 'number', ':number' => 2}, 'name_index')
240
+
241
+ expect(results.length).to eq(2)
242
+ end
243
+
244
+ it 'should count all items within an index partition that match a filter expression' do
245
+ subject.table_name = 'query_index'
246
+
247
+ count = subject.query(:name, 'name 1', nil, nil, '#number > :number', { '#number' => 'number', ':number' => 2}, 'name_index', nil, true)
248
+
249
+ expect(count).to eq(2)
250
+ end
251
+
252
+ after do
253
+ schema_manager.drop('query_index')
254
+ end
255
+ end
256
+
257
+ context '#scan' do
258
+ before do
259
+ schema_manager.drop('scan')
260
+
261
+ attributes_builder = DynamoDbFramework::AttributesBuilder.new
262
+ attributes_builder.add(:id, :S)
263
+
264
+ schema_manager.create('scan', attributes_builder.attributes, :id)
265
+
266
+
267
+ create_query_item('name 1', 1, 'scan')
268
+ create_query_item('name 1', 2, 'scan')
269
+ create_query_item('name 1', 3, 'scan')
270
+ create_query_item('name 1', 4, 'scan')
271
+ create_query_item('name 2', 1, 'scan')
272
+ create_query_item('name 2', 2, 'scan')
273
+ create_query_item('name 2', 3, 'scan')
274
+ create_query_item('name 3', 1, 'scan')
275
+ create_query_item('name 3', 2, 'scan')
276
+ end
277
+
278
+ it 'should return all items from a table' do
279
+ subject.table_name = 'scan'
280
+
281
+ results = subject.all()
282
+
283
+ expect(results.length).to eq(9)
284
+ end
285
+
286
+ it 'should count all items from a table that match a filter expression' do
287
+ subject.table_name = 'scan'
288
+
289
+ count = subject.scan('#name = :name', { '#name' => :name, ':name' => 'name 1' }, nil, true)
290
+
291
+ expect(count).to eq(4)
292
+ end
293
+
294
+ it 'should return all items from a table that match a filter expression' do
295
+ subject.table_name = 'scan'
296
+
297
+ results = subject.scan('#name = :name', { '#name' => :name, ':name' => 'name 1' })
298
+
299
+ expect(results.length).to eq(4)
300
+ end
301
+
302
+ after do
303
+ schema_manager.drop('scan')
304
+ end
305
+ end
306
+ end