clientside_aws 0.0.17

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Dockerfile +46 -0
  4. data/Gemfile +23 -0
  5. data/Gemfile.lock +99 -0
  6. data/README.md +105 -0
  7. data/bin/clientside_aws_build +6 -0
  8. data/bin/clientside_aws_run +5 -0
  9. data/bin/clientside_aws_test +4 -0
  10. data/clientside_aws.gemspec +31 -0
  11. data/clientside_aws/dynamodb.rb +722 -0
  12. data/clientside_aws/ec2.rb +103 -0
  13. data/clientside_aws/elastic_transcoder.rb +179 -0
  14. data/clientside_aws/firehose.rb +13 -0
  15. data/clientside_aws/kinesis.rb +13 -0
  16. data/clientside_aws/mock/core.rb +7 -0
  17. data/clientside_aws/mock/firehose.rb +14 -0
  18. data/clientside_aws/mock/kinesis.rb +18 -0
  19. data/clientside_aws/mock/s3.rb +59 -0
  20. data/clientside_aws/mock/ses.rb +74 -0
  21. data/clientside_aws/mock/sns.rb +17 -0
  22. data/clientside_aws/s3.rb +223 -0
  23. data/clientside_aws/ses.rb +9 -0
  24. data/clientside_aws/sns.rb +41 -0
  25. data/clientside_aws/sqs.rb +233 -0
  26. data/docker/clientside-aws-run +3 -0
  27. data/docker/redis-server-run +2 -0
  28. data/index.rb +57 -0
  29. data/lib/clientside_aws.rb +27 -0
  30. data/lib/clientside_aws/configuration.rb +14 -0
  31. data/lib/clientside_aws/mock.rb +224 -0
  32. data/lib/clientside_aws/version.rb +3 -0
  33. data/public/images/jscruff.jpg +0 -0
  34. data/public/images/spacer.gif +0 -0
  35. data/public/images/stock_video.mp4 +0 -0
  36. data/spec/dynamodb_spec.rb +1069 -0
  37. data/spec/ec2_spec.rb +138 -0
  38. data/spec/firehose_spec.rb +16 -0
  39. data/spec/kinesis_spec.rb +22 -0
  40. data/spec/s3_spec.rb +219 -0
  41. data/spec/sns_spec.rb +72 -0
  42. data/spec/spec_helper.rb +71 -0
  43. data/spec/sqs_spec.rb +87 -0
  44. data/spec/test_client/test.rb +45 -0
  45. data/spec/transcoder_spec.rb +138 -0
  46. metadata +241 -0
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ exec /usr/bin/ruby /opt/clientside_aws/index.rb -o 0.0.0.0
3
+
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ exec /usr/local/bin/redis-server /etc/redis/redis.conf >> /var/log/redis.log 2>&1
data/index.rb ADDED
@@ -0,0 +1,57 @@
1
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/"
2
+
3
+ require 'rubygems'
4
+ require 'sinatra'
5
+ require 'json'
6
+ require 'redis'
7
+ require 'bigdecimal'
8
+ require 'builder'
9
+ require 'digest'
10
+ require 'uuid'
11
+ require 'base64'
12
+ require 'rack'
13
+ require 'rack/cors'
14
+ require 'rack/protection'
15
+
16
+ ENV['RACK_ENV'] = 'development' unless ENV['RACK_ENV']
17
+
18
+ require 'clientside_aws/mock/core'
19
+
20
+ require 'clientside_aws/dynamodb'
21
+ require 'clientside_aws/sqs'
22
+ require 'clientside_aws/s3'
23
+ require 'clientside_aws/ec2'
24
+ require 'clientside_aws/ses'
25
+ require 'clientside_aws/elastic_transcoder'
26
+ require 'clientside_aws/sns'
27
+ require 'clientside_aws/kinesis'
28
+ require 'clientside_aws/firehose'
29
+
30
+ options = if defined?(Sinatra::Base.settings.clientside_aws_testing) && \
31
+ Sinatra::Base.settings.clientside_aws_testing
32
+ { host: 'localhost', port: 6380, timeout: 10 }
33
+ elsif ENV.key?('REDIS_HOST') && ENV.key?('REDIS_PORT')
34
+ { host: ENV['REDIS_HOST'],
35
+ port: ENV['REDIS_PORT'].to_i }
36
+ else
37
+ # Use localhost port 6379
38
+ {}
39
+ end
40
+
41
+ AWS_REDIS = Redis.new(options)
42
+
43
+ configure :development do
44
+ use Rack::Cors do
45
+ allow do
46
+ origins '*'
47
+ resource '*', headers: :any, methods: [:get, :post, :options, :put]
48
+ end
49
+ end
50
+ set :protection, except: [:http_origin]
51
+ end
52
+
53
+ DYNAMODB_PREFIX = 'DynamoDBv20110924'.freeze
54
+
55
+ get '/' do
56
+ 'hello'
57
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Class to help you mock requests to AWS to instead point to a local
4
+ # docker container
5
+
6
+ require File.dirname(__FILE__) + '/clientside_aws/configuration'
7
+
8
+ module ClientsideAws
9
+ require File.dirname(__FILE__) + '/clientside_aws/version'
10
+ require File.dirname(__FILE__) + '/clientside_aws/mock'
11
+
12
+ class << self
13
+ attr_accessor :configuration
14
+ end
15
+
16
+ def self.configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def self.reset
21
+ @configuration = Configuration.new
22
+ end
23
+
24
+ def self.configure
25
+ yield(configuration)
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpfully explained here: http://lizabinante.com/blog/creating-a-configurable-ruby-gem/
4
+ module ClientsideAws
5
+ class Configuration
6
+ attr_accessor :host
7
+ attr_accessor :port
8
+
9
+ def initialize
10
+ @host = 'aws'
11
+ @port = 4567
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Include this file in separate projects when you want to redirect
4
+ # requests that normally would go to the AWS production infrastrure and
5
+ # instead route them to a container you are running locally
6
+
7
+ require_relative '../../clientside_aws/mock/core'
8
+ require_relative '../../clientside_aws/mock/s3'
9
+ require_relative '../../clientside_aws/mock/ses'
10
+ require_relative '../../clientside_aws/mock/sns'
11
+ require_relative '../../clientside_aws/mock/kinesis'
12
+ require_relative '../../clientside_aws/mock/firehose'
13
+
14
+ require 'httparty'
15
+ # We do NOT use webmock/rspec because it removes the matchers after every test
16
+ # this breaks MET which is expecting to be able to communicate with AWS
17
+ # in the before(:all) rspec block
18
+ # Thus we just manually include what we needed from webmock/rspec and
19
+ # did not include the code to remove matchers after every test
20
+ require 'webmock'
21
+ require 'rspec'
22
+
23
+ WebMock.enable!
24
+ WebMock.allow_net_connect!
25
+
26
+ # WebMock.before_request do |request_signature, response|
27
+ # puts "Request #{request_signature} was made and #{response} was returned"
28
+ # binding.pry
29
+ # end
30
+
31
+ # Helper methods used to mock requests from code (either in development or test)
32
+ # to our container -- thus requiring HTTParty
33
+ def mock_uri(uri:)
34
+ uri.scheme = 'http'
35
+ uri.path = uri.host + uri.path
36
+ uri.host = ClientsideAws.configuration.host
37
+ uri.port = ClientsideAws.configuration.port
38
+
39
+ uri
40
+ end
41
+
42
+ def mock_post(request:)
43
+ response = HTTParty.post(mock_uri(uri: request.uri),
44
+ body: request.body,
45
+ headers: \
46
+ request.headers.merge('SERVER_NAME' => \
47
+ request.uri.host))
48
+
49
+ { headers: response.headers,
50
+ status: response.code,
51
+ body: response.body }
52
+ end
53
+
54
+ def mock_get(request:)
55
+ response = HTTParty.get(mock_uri(uri: request.uri),
56
+ query: request.uri.query_values,
57
+ headers: \
58
+ request.headers.merge('SERVER_NAME' => \
59
+ request.uri.host))
60
+
61
+ { headers: response.headers,
62
+ status: response.code,
63
+ body: response.body }
64
+ end
65
+
66
+ def mock_head(request:)
67
+ response = HTTParty.get(mock_uri(uri: request.uri),
68
+ query: request.uri.query_values,
69
+ headers: \
70
+ request.headers.merge('SERVER_NAME' => \
71
+ request.uri.host))
72
+
73
+ { headers: response.headers,
74
+ status: response.code,
75
+ body: '' }
76
+ end
77
+
78
+ def mock_put(request:)
79
+ response = HTTParty.put(mock_uri(uri: request.uri),
80
+ body: request.body,
81
+ headers: \
82
+ request.headers.merge('SERVER_NAME' => \
83
+ request.uri.host))
84
+
85
+ { headers: response.headers,
86
+ status: response.code,
87
+ body: response.body }
88
+ end
89
+
90
+ def mock_delete(request:)
91
+ response = HTTParty.delete(mock_uri(uri: request.uri),
92
+ query: request.uri.query_values,
93
+ headers: \
94
+ request.headers.merge('SERVER_NAME' => \
95
+ request.uri.host))
96
+
97
+ { headers: response.headers,
98
+ status: response.code,
99
+ body: response.body }
100
+ end
101
+
102
+ # Use WebMock to intercept all requests and redirect to our container
103
+ WebMock.stub_request(:post, %r{https?\:\/\/([\w\.]+\.us\-mockregion\-1)}) \
104
+ .to_return do |request|
105
+ mock_post(request: request)
106
+ end
107
+
108
+ WebMock.stub_request(:get, %r{https?\:\/\/([\w\.]+\.us\-mockregion\-1)}) \
109
+ .to_return do |request|
110
+ mock_get(request: request)
111
+ end
112
+
113
+ WebMock.stub_request(:head, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
114
+ .to_return do |request|
115
+ mock_head(request: request)
116
+ end
117
+
118
+ WebMock.stub_request(:put, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
119
+ .to_return do |request|
120
+ mock_put(request: request)
121
+ end
122
+
123
+ WebMock.stub_request(:delete, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
124
+ .to_return do |request|
125
+ mock_delete(request: request)
126
+ end
127
+
128
+ #
129
+ # Testing configuration
130
+ #
131
+ # In this case, we need to determine if we are running our own tests,
132
+ # in which case we use the rack/test request methods; otherwise use HTTParty
133
+ # to hit a separate test docker container
134
+ #
135
+
136
+ RSpec.configure do |config|
137
+ config.include WebMock::API
138
+ config.include WebMock::Matchers
139
+
140
+ config.before(:each) do
141
+ WebMock.reset!
142
+
143
+ clientside_aws_testing = \
144
+ defined?(Sinatra::Base.settings.clientside_aws_testing) && \
145
+ Sinatra::Base.settings.clientside_aws_testing
146
+
147
+ if clientside_aws_testing
148
+ # We are testing our own stuff; use rack/test methods
149
+ stub_request(:post, /us-mockregion-1/).to_return do |request|
150
+ post "/#{request.uri.host}",
151
+ request.body,
152
+ request.headers.merge('SERVER_NAME' => request.uri.host)
153
+
154
+ { headers: last_response.header,
155
+ status: last_response.status,
156
+ body: last_response.body }
157
+ end
158
+
159
+ stub_request(:get, /us-mockregion-1/).to_return do |request|
160
+ get "/#{request.uri.host}#{request.uri.path}",
161
+ request.uri.query_values,
162
+ request.headers.merge('SERVER_NAME' => request.uri.host)
163
+
164
+ { headers: last_response.header,
165
+ status: last_response.status,
166
+ body: last_response.body }
167
+ end
168
+
169
+ stub_request(:put, /us-mockregion-1/).to_return do |request|
170
+ put "/#{request.uri.host}#{request.uri.path}",
171
+ { body: request.body },
172
+ request.headers.merge('SERVER_NAME' => request.uri.host)
173
+
174
+ { headers: last_response.header,
175
+ status: last_response.status,
176
+ body: last_response.body }
177
+ end
178
+
179
+ stub_request(:head, /us-mockregion-1/).to_return do |request|
180
+ get "/#{request.uri.host}#{request.uri.path}",
181
+ { head_request: 1 }.merge(request.uri.query_values || {}),
182
+ request.headers.merge('SERVER_NAME' => request.uri.host)
183
+
184
+ { headers: last_response.header,
185
+ status: last_response.status,
186
+ body: '' }
187
+ end
188
+
189
+ stub_request(:delete, /us-mockregion-1/).to_return do |request|
190
+ delete "/#{request.uri.host}#{request.uri.path}",
191
+ request.uri.query_values,
192
+ request.headers.merge('SERVER_NAME' => request.uri.host)
193
+
194
+ { headers: last_response.header,
195
+ status: last_response.status,
196
+ body: last_response.body }
197
+ end
198
+ else
199
+ # A third-party has included us as a gem and is testing his own
200
+ # code; assume we have a test clientside_aws docker container
201
+ # running and hit that with HTTParty
202
+ stub_request(:post, %r{https?\:\/\/([\w\.]+\.us\-mockregion\-1|aws)}) \
203
+ .to_return do |request|
204
+ mock_post(request: request)
205
+ end
206
+ stub_request(:get, %r{https?\:\/\/([\w\.]+\.us\-mockregion\-1|aws)}) \
207
+ .to_return do |request|
208
+ mock_get(request: request)
209
+ end
210
+ stub_request(:head, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
211
+ .to_return do |request|
212
+ mock_head(request: request)
213
+ end
214
+ stub_request(:put, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
215
+ .to_return do |request|
216
+ mock_put(request: request)
217
+ end
218
+ stub_request(:delete, %r{https?\:\/\/[\w\.]+\.us\-mockregion\-1}) \
219
+ .to_return do |request|
220
+ mock_delete(request: request)
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,3 @@
1
+ module ClientsideAws
2
+ VERSION = '0.0.17'.freeze
3
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,1069 @@
1
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/../"
2
+
3
+ require 'spec/spec_helper'
4
+
5
+ describe 'Profiles Spec' do
6
+ include Rack::Test::Methods
7
+
8
+ before(:each) do
9
+ # Clean data after each test so we do not stomp on each other!
10
+ AWS_REDIS.flushall
11
+ end
12
+
13
+ def app
14
+ Sinatra::Application
15
+ end
16
+
17
+ it 'says hello' do
18
+ get '/'
19
+ expect(last_response).to be_ok
20
+ end
21
+
22
+ it 'v1: should handle basic CRUD test' do
23
+ dynamo_db = AWS::DynamoDB.new
24
+
25
+ test_table = dynamo_db.tables.create('test1', 1, 1,
26
+ hash_key: { creator_id: :number },
27
+ range_key: { date: :number })
28
+
29
+ expect(dynamo_db.tables.to_a.length).to eq 1
30
+ expect(dynamo_db.tables['test1'].exists?).to be true
31
+ # dynamo_db.tables['test_fake'].exists?.should be_false # this test fails for some reason
32
+
33
+ test_table.hash_key = [:creator_id, :number]
34
+ test_table.range_key = [:date, :number]
35
+
36
+ now = Time.now.to_f
37
+ test_table.items.put(creator_id: 10, date: now, data1: 'data1')
38
+ expect(test_table.items[10, now].exists?).to be true
39
+ expect(test_table.items[11, now].exists?).to be false
40
+
41
+ results = test_table.items.query(hash_value: 10, range_value: (0..(now + 1)))
42
+ expect(results.to_a.length).to eq 1
43
+ expect(results.to_a.first.attributes['creator_id']).to eq 10
44
+
45
+ results = test_table.items.query(hash_value: 10, range_value: 0..1)
46
+ expect(results.to_a.length).to eq 0
47
+
48
+ results = test_table.items.query(hash_value: 10, range_value: (0..(now + 1)))
49
+ results.to_a.first.attributes.update do |u|
50
+ u.add 'foo' => 'bar'
51
+ u.add 'bar' => 100
52
+ end
53
+
54
+ expect(test_table.items[10, now].attributes['foo']).to eq 'bar'
55
+ expect(test_table.items[10, now].attributes['bar']).to eq 100
56
+
57
+ test_table.items[10, now].attributes.update do |u|
58
+ u.delete 'foo'
59
+ end
60
+
61
+ expect(test_table.items[10, now].attributes['foo']).to be_nil
62
+
63
+ item = test_table.items[10, now]
64
+ item.delete
65
+ expect(test_table.items[10, now].exists?).to be false
66
+
67
+ test_table.delete
68
+ expect(dynamo_db.tables.to_a.length).to eq 0
69
+ end
70
+
71
+ it 'v2: should handle basic CRUD test' do
72
+ dynamo_db = Aws::DynamoDB::Client.new
73
+
74
+ dynamo_db.create_table(
75
+ table_name: 'test',
76
+ provisioned_throughput: {
77
+ read_capacity_units: 10,
78
+ write_capacity_units: 10
79
+ },
80
+ attribute_definitions: [{ attribute_name: :creator_id,
81
+ attribute_type: 'N' },
82
+ { attribute_name: :date,
83
+ attribute_type: 'N' }],
84
+ key_schema: [{ attribute_name: :creator_id, key_type: 'HASH' },
85
+ { attribute_name: :date, key_type: 'RANGE' }]
86
+ )
87
+
88
+ expect(dynamo_db.list_tables.table_names.length).to eq 1
89
+ expect(dynamo_db.list_tables.table_names.include?('test')).to be true
90
+
91
+ now = Time.now
92
+ dynamo_db.put_item(
93
+ table_name: 'test',
94
+ item: {
95
+ 'creator_id' => 10,
96
+ 'date' => now.to_f,
97
+ 'data1' => 'data1'
98
+ }
99
+ )
100
+
101
+ result1 = dynamo_db.get_item(table_name: 'test',
102
+ key: { 'creator_id' => 10,
103
+ 'date' => now.to_f })
104
+
105
+ result2 = dynamo_db.get_item(table_name: 'test',
106
+ key: { 'creator_id' => 11,
107
+ 'date' => now.to_f })
108
+
109
+ expect(result1.item).not_to be nil
110
+ expect(result2.item).to be nil
111
+
112
+ results = dynamo_db.query(
113
+ table_name: 'test',
114
+ scan_index_forward: false,
115
+ key_conditions: { 'creator_id' => { comparison_operator: 'EQ',
116
+ attribute_value_list: [10] },
117
+ 'date' => { comparison_operator: 'LT',
118
+ attribute_value_list: [(now + 1).to_f] } }
119
+ )
120
+
121
+ expect(results.count).to eq 1
122
+ expect(results.items.first['creator_id']).to eq 10
123
+
124
+ results = dynamo_db.query(
125
+ table_name: 'test',
126
+ scan_index_forward: true,
127
+ key_conditions: { 'creator_id' => { comparison_operator: 'EQ',
128
+ attribute_value_list: [10] },
129
+ 'date' => { comparison_operator: 'LT',
130
+ attribute_value_list: [1.to_f] } }
131
+ )
132
+ expect(results.count).to eq 0
133
+
134
+ dynamo_db.update_item(
135
+ table_name: 'test',
136
+ key: { 'creator_id' => 10,
137
+ 'date' => now.to_f },
138
+ attribute_updates: {
139
+ 'data1' => {
140
+ value: 'data2',
141
+ action: 'ADD'
142
+ },
143
+ 'foo' => {
144
+ value: 'bar',
145
+ action: 'ADD'
146
+ }
147
+ }
148
+ )
149
+
150
+ result1 = dynamo_db.get_item(table_name: 'test',
151
+ key: { 'creator_id' => 10,
152
+ 'date' => now.to_f })
153
+
154
+ expect(result1.item['foo']).to eq 'bar'
155
+ expect(result1.item['data1']).to eq 'data2'
156
+
157
+ dynamo_db.update_item(
158
+ table_name: 'test',
159
+ key: { 'creator_id' => 10,
160
+ 'date' => now.to_f },
161
+ attribute_updates: {
162
+ 'foo' => {
163
+ value: 'bar',
164
+ action: 'DELETE'
165
+ }
166
+ }
167
+ )
168
+
169
+ result1 = dynamo_db.get_item(table_name: 'test',
170
+ key: { 'creator_id' => 10,
171
+ 'date' => now.to_f })
172
+
173
+ expect(result1.item['foo']).to be nil
174
+
175
+ dynamo_db.delete_item(table_name: 'test',
176
+ key: { 'creator_id' => 10,
177
+ 'date' => now.to_f })
178
+
179
+ result1 = dynamo_db.get_item(table_name: 'test',
180
+ key: { 'creator_id' => 10,
181
+ 'date' => now.to_f })
182
+ expect(result1.item).to be nil
183
+
184
+ # Now delete table
185
+ dynamo_db.delete_table(table_name: 'test')
186
+ expect(dynamo_db.list_tables.table_names.length).to eq 0
187
+ end
188
+
189
+ it 'v1: test vistors' do
190
+ dynamo_db = AWS::DynamoDB.new
191
+
192
+ visitors_table = dynamo_db.tables.create('visitors', 10, 5,
193
+ hash_key: { creator_id: :number },
194
+ range_key: { date: :number })
195
+
196
+ visitors_table.hash_key = [:creator_id, :number]
197
+ visitors_table.range_key = [:date, :number]
198
+
199
+ (0..10).each do |idx|
200
+ visitors_table.items.put(creator_id: 1, date: Time.now.to_f - (60 * idx), target_id: 10 + idx)
201
+ end
202
+
203
+ ct = 0
204
+ results = visitors_table.items.query(hash_value: 1, scan_index_forward: false)
205
+ results.to_a.each do |item|
206
+ item.attributes['target_id'].to_i.should == 10 + ct
207
+ ct += 1
208
+ end
209
+
210
+ ct = 0
211
+ results = visitors_table.items.query(hash_value: 1)
212
+ results.to_a.each do |item|
213
+ item.attributes['target_id'].to_i.should == 20 - ct
214
+ ct += 1
215
+ end
216
+
217
+ visitors2_table = dynamo_db.tables.create('visitors2', 10, 5,
218
+ hash_key: { profile_id: :number },
219
+ range_key: { date_profile: :string })
220
+ visitors2_table.hash_key = [:profile_id, :number]
221
+ visitors2_table.range_key = [:date_profile, :string]
222
+
223
+ profile_id = 1000
224
+ (0..10).each do |idx|
225
+ timestamp = Time.now.to_f - (60 * idx)
226
+ visitors2_table.items.put(profile_id: idx, date_profile: "#{timestamp}:#{profile_id}", target_id: profile_id)
227
+ end
228
+ results = visitors2_table.items.query(hash_value: 1)
229
+ results.to_a.length.should == 1
230
+ end
231
+
232
+ it 'v2: test vistors' do
233
+ dynamo_db = Aws::DynamoDB::Client.new
234
+
235
+ dynamo_db.create_table(
236
+ table_name: 'visitors',
237
+ provisioned_throughput: {
238
+ read_capacity_units: 10,
239
+ write_capacity_units: 10
240
+ },
241
+ attribute_definitions: [{ attribute_name: :creator_id,
242
+ attribute_type: 'N' },
243
+ { attribute_name: :date,
244
+ attribute_type: 'N' }],
245
+ key_schema: [{ attribute_name: :creator_id, key_type: 'HASH' },
246
+ { attribute_name: :date, key_type: 'RANGE' }]
247
+ )
248
+
249
+ # Make some visitors
250
+ 10.times do |idx|
251
+ dynamo_db.put_item(
252
+ table_name: 'visitors',
253
+ item: {
254
+ 'creator_id' => 1,
255
+ 'date' => Time.now.to_f,
256
+ 'target_id' => 10 + idx
257
+ }
258
+ )
259
+ end
260
+
261
+ # Query both directions
262
+ results = dynamo_db.query(
263
+ table_name: 'visitors',
264
+ scan_index_forward: true,
265
+ key_conditions: { 'creator_id' => { comparison_operator: 'EQ',
266
+ attribute_value_list: [1] } }
267
+ )
268
+
269
+ ct = 0
270
+ results.items.each do |item|
271
+ expect(item['target_id'].to_i).to eq 10 + ct
272
+ ct += 1
273
+ end
274
+
275
+ results = dynamo_db.query(
276
+ table_name: 'visitors',
277
+ scan_index_forward: false,
278
+ key_conditions: { 'creator_id' => { comparison_operator: 'EQ',
279
+ attribute_value_list: [1] } }
280
+ )
281
+
282
+ ct = 0
283
+ results.items.each do |item|
284
+ expect(item['target_id'].to_i).to eq 19 - ct
285
+ ct += 1
286
+ end
287
+
288
+ # Nothing there
289
+ results = dynamo_db.query(
290
+ table_name: 'visitors',
291
+ scan_index_forward: false,
292
+ key_conditions: { 'creator_id' => { comparison_operator: 'EQ',
293
+ attribute_value_list: [200] } }
294
+ )
295
+ expect(results.count).to eq 0
296
+
297
+ # Make another table
298
+ dynamo_db.create_table(
299
+ table_name: 'visitors2',
300
+ provisioned_throughput: {
301
+ read_capacity_units: 10,
302
+ write_capacity_units: 10
303
+ },
304
+ attribute_definitions: [{ attribute_name: :profile_id,
305
+ attribute_type: 'N' },
306
+ { attribute_name: :date_profile,
307
+ attribute_type: 'S' }],
308
+ key_schema: [{ attribute_name: :profile_id, key_type: 'HASH' },
309
+ { attribute_name: :date_profile, key_type: 'RANGE' }]
310
+ )
311
+
312
+ profile_id = 1000
313
+
314
+ 10.times do |idx|
315
+ timestamp = Time.now.to_f - (60 * idx)
316
+ dynamo_db.put_item(
317
+ table_name: 'visitors2',
318
+ item: {
319
+ 'profile_id' => idx,
320
+ 'date_profile' => "#{timestamp}:#{profile_id}",
321
+ 'target_id' => profile_id
322
+ }
323
+ )
324
+ end
325
+
326
+ # Pull just one item out
327
+ results = dynamo_db.query(
328
+ table_name: 'visitors2',
329
+ scan_index_forward: false,
330
+ key_conditions: { 'profile_id' => { comparison_operator: 'EQ',
331
+ attribute_value_list: [1] } }
332
+ )
333
+ expect(results.count).to eq 1
334
+
335
+ # Test between
336
+ dynamo_db.put_item(
337
+ table_name: 'visitors',
338
+ item: {
339
+ 'creator_id' => 2,
340
+ 'date' => Time.now.to_f
341
+ }
342
+ )
343
+
344
+ dynamo_db.put_item(
345
+ table_name: 'visitors',
346
+ item: {
347
+ 'creator_id' => 2,
348
+ 'date' => (Time.now + 1).to_f
349
+ }
350
+ )
351
+
352
+ results = dynamo_db.query(
353
+ table_name: 'visitors',
354
+ scan_index_forward: false,
355
+ key_conditions: {
356
+ 'creator_id' => {
357
+ comparison_operator: 'EQ',
358
+ attribute_value_list: [2]
359
+ },
360
+ 'date' => {
361
+ comparison_operator: 'BETWEEN',
362
+ attribute_value_list: \
363
+ [(Time.now - 5).to_f,
364
+ (Time.now + 5).to_f]
365
+ }
366
+ }
367
+ )
368
+ expect(results.count).to eq 2
369
+ end
370
+
371
+ it 'v1: should handle create, delete' do
372
+ dynamo_db = AWS::DynamoDB::Client.new(api_version: '2012-08-10')
373
+
374
+ test_table = dynamo_db.create_table(
375
+ table_name: 'cd_table',
376
+ provisioned_throughput: { read_capacity_units: 1, write_capacity_units: 1 },
377
+ attribute_definitions: [
378
+ { attribute_name: 'profile_id', attribute_type: 'N' },
379
+ { attribute_name: 'visitor_id', attribute_type: 'N' }
380
+ ],
381
+ key_schema: [
382
+ { attribute_name: 'profile_id', key_type: 'HASH' },
383
+ { attribute_name: 'visitor_id', key_type: 'RANGE' }
384
+ ],
385
+ local_secondary_indexes: [{
386
+ index_name: 'cd_ls_index',
387
+ key_schema: [
388
+ { attribute_name: 'profile_id', key_type: 'HASH' },
389
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
390
+ ],
391
+ projection: { projection_type: 'ALL' }
392
+ }],
393
+ global_secondary_indexes: [{
394
+ index_name: 'cd_gs_index',
395
+ key_schema: [
396
+ { attribute_name: 'visitor_id', key_type: 'HASH' },
397
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
398
+ ],
399
+ projection: { projection_type: 'ALL' },
400
+ provisioned_throughput: { read_capacity_units: 1, write_capacity_units: 1 }
401
+ }]
402
+ )
403
+ dynamo_db.put_item(table_name: 'cd_table', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' },
404
+ 'timestamp' => { 'n' => 3.to_s } })
405
+
406
+ response = dynamo_db.get_item(table_name: 'cd_table', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
407
+ expect(response[:item]).not_to be_nil
408
+
409
+ # Test query
410
+ results = dynamo_db.query(table_name: 'cd_table', index_name: 'cd_gs_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
411
+ 'profile_id' => {
412
+ comparison_operator: 'EQ',
413
+ attribute_value_list: [
414
+ { 'n' => '2' }
415
+ ]
416
+ },
417
+ 'timestamp' => {
418
+ comparison_operator: 'LE',
419
+ attribute_value_list: [
420
+ { 'n' => 3.to_s }
421
+ ]
422
+ }
423
+ })
424
+ results[:member].length.should == 1
425
+
426
+ dynamo_db.delete_item(table_name: 'cd_table', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
427
+
428
+ response = dynamo_db.get_item(table_name: 'cd_table', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
429
+ expect(response[:item]).to be_nil
430
+
431
+ # Test query
432
+ results = dynamo_db.query(table_name: 'cd_table', index_name: 'cd_gs_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
433
+ 'profile_id' => {
434
+ comparison_operator: 'EQ',
435
+ attribute_value_list: [
436
+ { 'n' => '2' }
437
+ ]
438
+ },
439
+ 'timestamp' => {
440
+ comparison_operator: 'LE',
441
+ attribute_value_list: [
442
+ { 'n' => 3.to_s }
443
+ ]
444
+ }
445
+ })
446
+ results[:member].length.should == 0
447
+ end
448
+
449
+ it 'v2: should handle create, delete' do
450
+ dynamo_db = Aws::DynamoDB::Client.new
451
+
452
+ dynamo_db.create_table(
453
+ table_name: 'cd_table',
454
+ provisioned_throughput: { read_capacity_units: 1,
455
+ write_capacity_units: 1 },
456
+ attribute_definitions: [
457
+ { attribute_name: 'profile_id', attribute_type: 'N' },
458
+ { attribute_name: 'visitor_id', attribute_type: 'N' }
459
+ ],
460
+ key_schema: [
461
+ { attribute_name: 'profile_id', key_type: 'HASH' },
462
+ { attribute_name: 'visitor_id', key_type: 'RANGE' }
463
+ ],
464
+ local_secondary_indexes: [{
465
+ index_name: 'cd_ls_index',
466
+ key_schema: [
467
+ { attribute_name: 'profile_id', key_type: 'HASH' },
468
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
469
+ ],
470
+ projection: { projection_type: 'ALL' }
471
+ }],
472
+ global_secondary_indexes: [{
473
+ index_name: 'cd_gs_index',
474
+ key_schema: [
475
+ { attribute_name: 'visitor_id', key_type: 'HASH' },
476
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
477
+ ],
478
+ projection: { projection_type: 'ALL' },
479
+ provisioned_throughput: { read_capacity_units: 1,
480
+ write_capacity_units: 1 }
481
+ }]
482
+ )
483
+
484
+ dynamo_db.put_item(table_name: 'cd_table',
485
+ item: { 'profile_id' => 1,
486
+ 'visitor_id' => 2,
487
+ 'timestamp' => 3 })
488
+
489
+ response = dynamo_db.get_item(
490
+ table_name: 'cd_table',
491
+ key: { 'profile_id' => 1,
492
+ 'visitor_id' => 2 }
493
+ )
494
+ expect(response[:item]).not_to be_nil
495
+
496
+ # Test query
497
+ results = dynamo_db.query(
498
+ table_name: 'cd_table',
499
+ index_name: 'cd_gs_index',
500
+ select: 'ALL_PROJECTED_ATTRIBUTES',
501
+ key_conditions: {
502
+ 'profile_id' => {
503
+ comparison_operator: 'EQ',
504
+ attribute_value_list: [2]
505
+ },
506
+ 'timestamp' => {
507
+ comparison_operator: 'LE',
508
+ attribute_value_list: [3]
509
+ }
510
+ }
511
+ )
512
+ expect(results.count).to eq 1
513
+
514
+ dynamo_db.delete_item(
515
+ table_name: 'cd_table',
516
+ key: { 'profile_id' => 1, 'visitor_id' => 2 }
517
+ )
518
+
519
+ response = dynamo_db.get_item(
520
+ table_name: 'cd_table',
521
+ key: { 'profile_id' => 1, 'visitor_id' => 2 }
522
+ )
523
+ expect(response[:item]).to be_nil
524
+
525
+ # Test query
526
+ results = dynamo_db.query(
527
+ table_name: 'cd_table',
528
+ index_name: 'cd_gs_index',
529
+ select: 'ALL_PROJECTED_ATTRIBUTES',
530
+ key_conditions: {
531
+ 'profile_id' => {
532
+ comparison_operator: 'EQ',
533
+ attribute_value_list: [2]
534
+ },
535
+ 'timestamp' => {
536
+ comparison_operator: 'LE',
537
+ attribute_value_list: [3]
538
+ }
539
+ }
540
+ )
541
+
542
+ expect(results.count).to eq 0
543
+ end
544
+
545
+ it 'v1: should handle local secondary indexes' do
546
+ dynamo_db = AWS::DynamoDB::Client.new(api_version: '2012-08-10')
547
+
548
+ test_table = dynamo_db.create_table(
549
+ table_name: 'visited_by',
550
+ provisioned_throughput: { read_capacity_units: 1, write_capacity_units: 1 },
551
+ attribute_definitions: [
552
+ { attribute_name: 'profile_id', attribute_type: 'N' },
553
+ { attribute_name: 'visitor_id', attribute_type: 'N' }
554
+ ],
555
+ key_schema: [
556
+ { attribute_name: 'profile_id', key_type: 'HASH' },
557
+ { attribute_name: 'visitor_id', key_type: 'RANGE' }
558
+ ],
559
+ local_secondary_indexes: [{
560
+ index_name: 'ls_index',
561
+ key_schema: [
562
+ { attribute_name: 'profile_id', key_type: 'HASH' },
563
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
564
+ ],
565
+ projection: { projection_type: 'ALL' }
566
+ }],
567
+ global_secondary_indexes: [{
568
+ index_name: 'gs_index',
569
+ key_schema: [
570
+ { attribute_name: 'visitor_id', key_type: 'HASH' },
571
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
572
+ ],
573
+ projection: { projection_type: 'ALL' },
574
+ provisioned_throughput: { read_capacity_units: 1, write_capacity_units: 1 }
575
+ }]
576
+ )
577
+
578
+ now = Time.now.to_i
579
+
580
+ # Test put and get
581
+
582
+ # 2 visits 1
583
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' },
584
+ 'timestamp' => { 'n' => 3.to_s } })
585
+ item = dynamo_db.get_item(table_name: 'visited_by', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
586
+ item.should_not be_nil
587
+ item[:item]['profile_id'][:n].should == '1'
588
+ item[:item]['timestamp'][:n].should == '3'
589
+
590
+ # 2 visits 1 again
591
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' },
592
+ 'timestamp' => { 'n' => 4.to_s } })
593
+ item = dynamo_db.get_item(table_name: 'visited_by', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
594
+ item.should_not be_nil
595
+ item[:item]['profile_id'][:n].should == '1'
596
+ item[:item]['timestamp'][:n].should == '4'
597
+
598
+ # 2 visits 1 a third time, with timestamp of now
599
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' },
600
+ 'timestamp' => { 'n' => now.to_s } })
601
+
602
+ item = dynamo_db.get_item(table_name: 'visited_by', key: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '2' } })
603
+ item.should_not be_nil
604
+ item[:item]['profile_id'][:n].should == '1'
605
+
606
+ item = dynamo_db.get_item(table_name: 'visited_by', key: { 'profile_id' => { 'n' => '2' }, 'visitor_id' => { 'n' => '2' } })
607
+ expect(item[:item]).to be_nil
608
+
609
+ # Try the global secondary index
610
+ results = dynamo_db.query(table_name: 'visited_by', index_name: 'gs_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
611
+ 'profile_id' => {
612
+ comparison_operator: 'EQ',
613
+ attribute_value_list: [
614
+ { 'n' => '2' }
615
+ ]
616
+ },
617
+ 'timestamp' => {
618
+ comparison_operator: 'LE',
619
+ attribute_value_list: [
620
+ { 'n' => Time.now.to_i.to_s }
621
+ ]
622
+ }
623
+ })
624
+ results[:member].length.should == 1
625
+
626
+ # Try the local secondary index
627
+ results = dynamo_db.query(table_name: 'visited_by', index_name: 'ls_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
628
+ 'profile_id' => {
629
+ comparison_operator: 'EQ',
630
+ attribute_value_list: [
631
+ { 'n' => '1' }
632
+ ]
633
+ },
634
+ 'timestamp' => {
635
+ comparison_operator: 'LE',
636
+ attribute_value_list: [
637
+ { 'n' => Time.now.to_i.to_s }
638
+ ]
639
+ }
640
+ })
641
+ results[:member].length.should == 1
642
+
643
+ results = dynamo_db.query(table_name: 'visited_by', index_name: 'ls_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
644
+ 'profile_id' => {
645
+ comparison_operator: 'EQ',
646
+ attribute_value_list: [
647
+ { 'n' => '1' }
648
+ ]
649
+ },
650
+ 'timestamp' => {
651
+ comparison_operator: 'LE',
652
+ attribute_value_list: [
653
+ { 'n' => (Time.now.utc.to_i - 2).to_s }
654
+ ]
655
+ }
656
+ })
657
+ results[:member].length.should == 0
658
+
659
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '3' }, 'timestamp' => { 'n' => Time.now.utc.to_i.to_s } })
660
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => '1' }, 'visitor_id' => { 'n' => '4' }, 'timestamp' => { 'n' => Time.now.utc.to_i.to_s } })
661
+
662
+ results = dynamo_db.query(table_name: 'visited_by', index_name: 'ls_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
663
+ 'profile_id' => {
664
+ comparison_operator: 'EQ',
665
+ attribute_value_list: [
666
+ { 'n' => '1' }
667
+ ]
668
+ },
669
+ 'timestamp' => {
670
+ comparison_operator: 'LE',
671
+ attribute_value_list: [
672
+ { 'n' => Time.now.to_i.to_s }
673
+ ]
674
+ }
675
+ })
676
+ results[:member].length.should == 3
677
+
678
+ # Add some more profiles visited by 2
679
+ (3...10).each do |idx|
680
+ dynamo_db.put_item(table_name: 'visited_by', item: { 'profile_id' => { 'n' => idx.to_s }, 'visitor_id' => { 'n' => '2' },
681
+ 'timestamp' => { 'n' => (now - idx).to_s } })
682
+ end
683
+
684
+ results = dynamo_db.query(table_name: 'visited_by', index_name: 'gs_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
685
+ 'profile_id' => {
686
+ comparison_operator: 'EQ',
687
+ attribute_value_list: [
688
+ { 'n' => '2' }
689
+ ]
690
+ },
691
+ 'timestamp' => {
692
+ comparison_operator: 'LE',
693
+ attribute_value_list: [
694
+ { 'n' => Time.now.to_i.to_s }
695
+ ]
696
+ }
697
+ })
698
+ results[:member].length.should == 8
699
+ results[:member].first['profile_id'][:n].should == '9'
700
+ results[:member].last['profile_id'][:n].should == '1'
701
+
702
+ # reverse
703
+ results = dynamo_db.query(table_name: 'visited_by',
704
+ scan_index_forward: false,
705
+ index_name: 'gs_index', select: 'ALL_PROJECTED_ATTRIBUTES', key_conditions: {
706
+ 'profile_id' => {
707
+ comparison_operator: 'EQ',
708
+ attribute_value_list: [
709
+ { 'n' => '2' }
710
+ ]
711
+ },
712
+ 'timestamp' => {
713
+ comparison_operator: 'LE',
714
+ attribute_value_list: [
715
+ { 'n' => Time.now.to_i.to_s }
716
+ ]
717
+ }
718
+ })
719
+ results[:member].length.should == 8
720
+ results[:member].first['profile_id'][:n].should == '1'
721
+ results[:member].last['profile_id'][:n].should == '9'
722
+ end
723
+
724
+ it 'v2: should handle local secondary indexes' do
725
+ dynamo_db = Aws::DynamoDB::Client.new
726
+
727
+ dynamo_db.create_table(
728
+ table_name: 'visited_by',
729
+ provisioned_throughput: { read_capacity_units: 1,
730
+ write_capacity_units: 1 },
731
+ attribute_definitions: [
732
+ { attribute_name: 'profile_id', attribute_type: 'N' },
733
+ { attribute_name: 'visitor_id', attribute_type: 'N' }
734
+ ],
735
+ key_schema: [
736
+ { attribute_name: 'profile_id', key_type: 'HASH' },
737
+ { attribute_name: 'visitor_id', key_type: 'RANGE' }
738
+ ],
739
+ local_secondary_indexes: [{
740
+ index_name: 'ls_index',
741
+ key_schema: [
742
+ { attribute_name: 'profile_id', key_type: 'HASH' },
743
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
744
+ ],
745
+ projection: { projection_type: 'ALL' }
746
+ }],
747
+ global_secondary_indexes: [{
748
+ index_name: 'gs_index',
749
+ key_schema: [
750
+ { attribute_name: 'visitor_id', key_type: 'HASH' },
751
+ { attribute_name: 'timestamp', key_type: 'RANGE' }
752
+ ],
753
+ projection: { projection_type: 'ALL' },
754
+ provisioned_throughput: { read_capacity_units: 1,
755
+ write_capacity_units: 1 }
756
+ }]
757
+ )
758
+
759
+ now = Time.now.to_i
760
+
761
+ # Test put and get
762
+
763
+ # 2 visits 1
764
+ dynamo_db.put_item(
765
+ table_name: 'visited_by',
766
+ item: { 'profile_id' => 1,
767
+ 'visitor_id' => 2,
768
+ 'timestamp' => 3 }
769
+ )
770
+ item = dynamo_db.get_item(
771
+ table_name: 'visited_by',
772
+ key: { 'profile_id' => 1,
773
+ 'visitor_id' => 2 }
774
+ )
775
+ expect(item.item).not_to be nil
776
+ expect(item.item['profile_id']).to eq 1
777
+ expect(item.item['timestamp']).to eq 3
778
+
779
+ # 2 visits 1 again
780
+ dynamo_db.put_item(
781
+ table_name: 'visited_by',
782
+ item: { 'profile_id' => 1,
783
+ 'visitor_id' => 2,
784
+ 'timestamp' => 4 }
785
+ )
786
+
787
+ item = dynamo_db.get_item(
788
+ table_name: 'visited_by',
789
+ key: { 'profile_id' => 1,
790
+ 'visitor_id' => 2 }
791
+ )
792
+ expect(item.item).not_to be nil
793
+ expect(item.item['profile_id']).to eq 1
794
+ expect(item.item['timestamp']).to eq 4
795
+
796
+ # 2 visits 1 a third time, with timestamp of now
797
+ dynamo_db.put_item(
798
+ table_name: 'visited_by',
799
+ item: { 'profile_id' => 1,
800
+ 'visitor_id' => 2,
801
+ 'timestamp' => now }
802
+ )
803
+ item = dynamo_db.get_item(
804
+ table_name: 'visited_by',
805
+ key: { 'profile_id' => 1,
806
+ 'visitor_id' => 2 }
807
+ )
808
+ expect(item.item).not_to be nil
809
+ expect(item.item['profile_id']).to eq 1
810
+
811
+ item = dynamo_db.get_item(
812
+ table_name: 'visited_by',
813
+ key: { 'profile_id' => 2,
814
+ 'visitor_id' => 2 }
815
+ )
816
+ expect(item.item).to be nil
817
+
818
+ # Try the global secondary index
819
+ results = dynamo_db.query(
820
+ table_name: 'visited_by',
821
+ index_name: 'gs_index',
822
+ select: 'ALL_PROJECTED_ATTRIBUTES',
823
+ key_conditions: {
824
+ 'profile_id' => {
825
+ comparison_operator: 'EQ',
826
+ attribute_value_list: [2]
827
+ },
828
+ 'timestamp' => {
829
+ comparison_operator: 'LE',
830
+ attribute_value_list: [Time.now.to_i]
831
+ }
832
+ }
833
+ )
834
+ expect(results.count).to eq 1
835
+
836
+ # Try the local secondary index
837
+ results = dynamo_db.query(
838
+ table_name: 'visited_by',
839
+ index_name: 'ls_index',
840
+ select: 'ALL_PROJECTED_ATTRIBUTES',
841
+ key_conditions: {
842
+ 'profile_id' => {
843
+ comparison_operator: 'EQ',
844
+ attribute_value_list: [1]
845
+ },
846
+ 'timestamp' => {
847
+ comparison_operator: 'LE',
848
+ attribute_value_list: [Time.now.to_i]
849
+ }
850
+ }
851
+ )
852
+ expect(results.count).to eq 1
853
+
854
+ results = dynamo_db.query(
855
+ table_name: 'visited_by',
856
+ index_name: 'ls_index',
857
+ select: 'ALL_PROJECTED_ATTRIBUTES',
858
+ key_conditions: {
859
+ 'profile_id' => {
860
+ comparison_operator: 'EQ',
861
+ attribute_value_list: [1]
862
+ },
863
+ 'timestamp' => {
864
+ comparison_operator: 'LE',
865
+ attribute_value_list: [Time.now.to_i - 2]
866
+ }
867
+ }
868
+ )
869
+ expect(results.count).to eq 0
870
+
871
+ # 3 and 4 visit
872
+ (3..4).each do |visitor_id|
873
+ dynamo_db.put_item(
874
+ table_name: 'visited_by',
875
+ item: { 'profile_id' => 1,
876
+ 'visitor_id' => visitor_id,
877
+ 'timestamp' => Time.now.to_i }
878
+ )
879
+ end
880
+
881
+ results = dynamo_db.query(
882
+ table_name: 'visited_by',
883
+ index_name: 'ls_index',
884
+ select: 'ALL_PROJECTED_ATTRIBUTES',
885
+ key_conditions: {
886
+ 'profile_id' => {
887
+ comparison_operator: 'EQ',
888
+ attribute_value_list: [1]
889
+ },
890
+ 'timestamp' => {
891
+ comparison_operator: 'LE',
892
+ attribute_value_list: [Time.now.to_i]
893
+ }
894
+ }
895
+ )
896
+
897
+ expect(results.count).to eq 3
898
+
899
+ # Add some more profiles visited by 2
900
+ (3...10).each do |idx|
901
+ dynamo_db.put_item(
902
+ table_name: 'visited_by',
903
+ item: { 'profile_id' => idx,
904
+ 'visitor_id' => 2,
905
+ 'timestamp' => (now - idx).to_i }
906
+ )
907
+ end
908
+
909
+ results = dynamo_db.query(
910
+ table_name: 'visited_by',
911
+ index_name: 'gs_index',
912
+ select: 'ALL_PROJECTED_ATTRIBUTES',
913
+ key_conditions: {
914
+ 'profile_id' => {
915
+ comparison_operator: 'EQ',
916
+ attribute_value_list: [2]
917
+ },
918
+ 'timestamp' => {
919
+ comparison_operator: 'LE',
920
+ attribute_value_list: [Time.now.to_i]
921
+ }
922
+ }
923
+ )
924
+ expect(results.count).to eq 8
925
+ expect(results.items.first['profile_id']).to eq 9
926
+ expect(results.items.last['profile_id']).to eq 1
927
+
928
+ # reverse
929
+ results = dynamo_db.query(
930
+ table_name: 'visited_by',
931
+ index_name: 'gs_index',
932
+ scan_index_forward: false,
933
+ select: 'ALL_PROJECTED_ATTRIBUTES',
934
+ key_conditions: {
935
+ 'profile_id' => {
936
+ comparison_operator: 'EQ',
937
+ attribute_value_list: [2]
938
+ },
939
+ 'timestamp' => {
940
+ comparison_operator: 'LE',
941
+ attribute_value_list: [Time.now.to_i]
942
+ }
943
+ }
944
+ )
945
+ expect(results.count).to eq 8
946
+ expect(results.items.first['profile_id']).to eq 1
947
+ expect(results.items.last['profile_id']).to eq 9
948
+ end
949
+
950
+ it 'v1: should handle update item' do
951
+ dynamo_db = AWS::DynamoDB::Client.new(api_version: '2012-08-10')
952
+
953
+ test_table = dynamo_db.create_table(
954
+ table_name: 'visitor_counts',
955
+ provisioned_throughput: { read_capacity_units: 1, write_capacity_units: 1 },
956
+ attribute_definitions: [
957
+ { attribute_name: 'profile_id', attribute_type: 'N' },
958
+ { attribute_name: 'count', attribute_type: 'N' }
959
+ ],
960
+ key_schema: [
961
+ { attribute_name: 'profile_id', key_type: 'HASH' },
962
+ { attribute_name: 'count', key_type: 'RANGE' }
963
+ ],
964
+ local_secondary_indexes: [{
965
+ index_name: 'ls_index',
966
+ key_schema: [
967
+ { attribute_name: 'profile_id', key_type: 'HASH' },
968
+ { attribute_name: 'count', key_type: 'RANGE' }
969
+ ],
970
+ projection: { projection_type: 'ALL' }
971
+ }]
972
+ )
973
+
974
+ dynamo_db.update_item(table_name: 'visitor_counts',
975
+ key: { 'profile_id' => { 'n' => 1.to_s } },
976
+ attribute_updates: {
977
+ 'count' => { action: 'ADD', value: { 'n' => 1.to_s } }
978
+ })
979
+ end
980
+
981
+ it 'v2: should handle update item' do
982
+ dynamo_db = Aws::DynamoDB::Client.new
983
+
984
+ dynamo_db.create_table(
985
+ table_name: 'visitor_counts',
986
+ provisioned_throughput: { read_capacity_units: 1,
987
+ write_capacity_units: 1 },
988
+ attribute_definitions: [
989
+ { attribute_name: 'profile_id', attribute_type: 'N' },
990
+ { attribute_name: 'count', attribute_type: 'N' }
991
+ ],
992
+ key_schema: [
993
+ { attribute_name: 'profile_id', key_type: 'HASH' },
994
+ { attribute_name: 'count', key_type: 'RANGE' }
995
+ ],
996
+ local_secondary_indexes: [{
997
+ index_name: 'ls_index',
998
+ key_schema: [
999
+ { attribute_name: 'profile_id', key_type: 'HASH' },
1000
+ { attribute_name: 'count', key_type: 'RANGE' }
1001
+ ],
1002
+ projection: { projection_type: 'ALL' }
1003
+ }]
1004
+ )
1005
+
1006
+ dynamo_db.update_item(
1007
+ table_name: 'visitor_counts',
1008
+ key: { 'profile_id' => 1 },
1009
+ attribute_updates: {
1010
+ 'count' => { action: 'ADD', value: 1 }
1011
+ }
1012
+ )
1013
+ dynamo_db.update_item(
1014
+ table_name: 'visitor_counts',
1015
+ key: { 'profile_id' => 1 },
1016
+ attribute_updates: {
1017
+ 'count' => { action: 'ADD', value: 1 }
1018
+ }
1019
+ )
1020
+
1021
+ result1 = dynamo_db.get_item(table_name: 'visitor_counts',
1022
+ key: { 'profile_id' => 1 })
1023
+ expect(result1.item['count']).to eq 2
1024
+ end
1025
+
1026
+ it 'v1: should handle get item when no values' do
1027
+ dynamo_db = AWS::DynamoDB::Client.new(api_version: '2012-08-10')
1028
+
1029
+ dynamo_db.create_table(
1030
+ table_name: 'secrets',
1031
+ provisioned_throughput: \
1032
+ { read_capacity_units: 1, write_capacity_units: 1 },
1033
+ attribute_definitions: [
1034
+ { attribute_name: 'name', attribute_type: 'S' }
1035
+ ],
1036
+ key_schema: [
1037
+ { attribute_name: 'name', key_type: 'HASH' }
1038
+ ]
1039
+ )
1040
+ value = dynamo_db.get_item(
1041
+ table_name: 'secrets',
1042
+ key: { 'name' => { 's' => 'hi'.to_s } }
1043
+ )
1044
+
1045
+ expect(value[:item]).to be nil
1046
+ end
1047
+
1048
+ it 'v2: should handle get item when no values' do
1049
+ dynamo_db = Aws::DynamoDB::Client.new
1050
+
1051
+ dynamo_db.create_table(
1052
+ table_name: 'secrets',
1053
+ provisioned_throughput: \
1054
+ { read_capacity_units: 1, write_capacity_units: 1 },
1055
+ attribute_definitions: [
1056
+ { attribute_name: 'name', attribute_type: 'S' }
1057
+ ],
1058
+ key_schema: [
1059
+ { attribute_name: 'name', key_type: 'HASH' }
1060
+ ]
1061
+ )
1062
+ value = dynamo_db.get_item(
1063
+ table_name: 'secrets',
1064
+ key: { 'name' => 'hi' }
1065
+ )
1066
+
1067
+ expect(value[:item]).to be nil
1068
+ end
1069
+ end