dynamodb_framework 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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