clientside_aws 0.0.17

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