rom-dynamodb 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/issue_template.md +0 -2
- data/.travis.yml +8 -10
- data/CHANGELOG.md +6 -0
- data/README.md +144 -4
- data/docker-compose.yml +7 -1
- data/lib/rom/dynamodb/commands/update.rb +1 -1
- data/lib/rom/dynamodb/dataset.rb +208 -11
- data/lib/rom/dynamodb/dataset/where_clause.rb +100 -0
- data/lib/rom/dynamodb/relation.rb +54 -11
- data/lib/rom/dynamodb/version.rb +1 -1
- data/rom-dynamodb.gemspec +2 -1
- metadata +21 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5dfe29ecc7e7f6bc3d64a8cdb9413034b1a36ee7
|
|
4
|
+
data.tar.gz: fab828906c5c5c0778f14632713b7dcbf82b970b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06a62bc7f70a74398de8fffc03aa0ba35995daf6915717a130c985155078e28d9a01dda35daf485de909f269b3b1eb463c2835c658f03ca6a8756a4d7d9aadec
|
|
7
|
+
data.tar.gz: 840044b23db325be06a58b74dc2d5861b7aa74c020e0d120ad72a635569f32526fd47d1b9dc5a8e10930ad2acc5b8092f71d62213ef8e40101dab9a2ce6e4118
|
data/.github/issue_template.md
CHANGED
data/.travis.yml
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
services:
|
|
2
|
+
- docker
|
|
2
3
|
|
|
3
4
|
sudo: required
|
|
4
5
|
|
|
@@ -6,12 +7,15 @@ before_install:
|
|
|
6
7
|
- docker-compose pull
|
|
7
8
|
- docker-compose build
|
|
8
9
|
|
|
9
|
-
script:
|
|
10
|
+
script:
|
|
11
|
+
- docker-compose run rom bundle exec rspec
|
|
12
|
+
|
|
13
|
+
after_success:
|
|
14
|
+
- bundle exec codeclimate-test-reporter
|
|
10
15
|
|
|
11
16
|
env:
|
|
12
17
|
global:
|
|
13
|
-
-
|
|
14
|
-
- TRAVIS=true
|
|
18
|
+
- secure: "y+WKE+O64Mk9/MIiVDsHY12FsIDv31c5kGT1PD8X3bGzDLSs2RsxH+6aAPKj8soQIU6vN1cpXzHOI9rmgSa/jPwXN/TVhXNQUEaQGaCQjlucx4D7M0SjXxrEVcFsXeQpkb+sDH7zeRDj9mxnpzpHzFc1uLBodKUKFoqDJP67rYzdfHYTGettvGxPBR38gZxSPGejVNbb+cnY8bvUECK/Qn+0MTR3xjkZi+y4PKsj4ZlN7+4m8GOQt/S/9SK++5MJtlXXRtC7tufXZ3QJvmyfB/FbDYm5/e+ovAWVFI2s62/IuE5qdoEjBK3j8cKvTErrBzp1mo9JtM1QeJGAGuOroM8ki9cjPV+mtuFdIq7pzjBOK8ZIWGCQCljuIvyWZ88jZQzqkjgcREvBxGJa5u/maaNduUXWdfR/Q2DFazX7SfrD5CLXvinemVdPpo8/D0NhduSW8pgSKF0E9WBGng2z3BP40L+l7aTFfzrrirdzDNU1KsOzfDDs9+h9Rq8uwIhKiZhiPr+4ztXteNmaJMaeL9KnAgIhZGW1EYUkC1gAVXRkzaFFh3tnRhKAzU1LJxl2qskf0MJFgc/hr3hTHvWgYCQYe2QCZdulqb7HdnUH55Twsha+XVtMp25GxKVrTN4ET5L971xALGvTRxKaO2lekZmVxr4qauhyoXMUc1pCN4g="
|
|
15
19
|
- secure: "kQuM1k5J7COei8KyvabP/YROhoUFlLZkieaDQLFyOj2cobYW6bYsfzuWkNKaNpBrVjEenumLHwHg6P9vVmI7FQ1IEUW2QkAYSUUni/jRBK53jICIINWo6YLWgLQdpdkBnCzMv8MVXBlrqLgqcUn3aUXEtOhM9uHQbhknBx+z+Yja8tdjt6m8scBmqGj8a3zH3Pypi0PbH7fNHUXzAGx/CUyB/s58zUkm9SRs7uR5TRVKtmUyXaroTxmKL9iDIuArjMsOTzu/Xr1kwdlg1kmzB//j0basRzYPOSNdJAGKEdwXo6ul7ZGc993iIJGS/+LZuLgrOcdjHklBSWlduQABEqOFpL76cG7RUH5Mo58Wqd+ftR/Lz5+vEDxpj1JXOUzKv4yB/EA3hTxnK7vCvp5pYSAD+sqtTn5f8TpcxaKYwR0n5YUtV9E8JhZ4I5QvWONU1zXsb9ulhGBFRhZniFKNEsBoapdCA+rAEXLJS0D7ODEE7degsv1uMhO6QPt1sAWvHZvZh01XvlEM9p7aeTOnD72DjKw47GTU/GL6Ekl8er+wubk/ZGBKMQ9YZXiT2jQwgIJyKSWw19s0Ru9otLhrSgwY0eHhvQuyYWL2DwFV2Bcrx1Glgj96+nSzj8g/42NTL1fX5cYAAPYjnqWLc8yuqF+ioF24Yq22CuTQhn/u0BE="
|
|
16
20
|
|
|
17
21
|
addons:
|
|
@@ -19,12 +23,6 @@ addons:
|
|
|
19
23
|
repo_token:
|
|
20
24
|
secure: "AL3aOEHyWvXkXGne+U3n6ToU0Ubp8yiy3y6gqo/IF2DsN133ynEwcUsif81eIeMTYa+LH4PfDV4iImbwrawhJdyr1ghAqC98k2xfzD5oBSOO8b6gj5k3fRLGRIlGcFwrSSZm9t3F3J3teU3QeaeOW5QhJrtGtlApVvMRzUQPmPbFKjSbzg9oXnl6rWpghUGPjkJZ1zJARZqYx6yJ5JYoRFX7xPcwmwvMa3tCnLh3wfpVCexsa33gssFCGUuh/qU2aLO9f/EyLV95nkxNoXdNoMAajuddDAUK2GQSBLuGijyydUl+x9ZLEsfMJbW3xFqeYVQzMiRXMku0kp7NuzSF9TfPP4TSv5EIJAuVujB3u7sc+jRGEy+m+WawUunu8BmX1l9vjBMcMUonqcsToTBHf9u1nLBdJMhwk0WQkvcJ31g4lrc5y4wut/mRdylBTrplAJAwcHfXnM7ADWL4DN3Yx+Vo+lPgkCrtnsMST2PUcyW2v+hln/BR9qB9PGPC121o7D+BNNuT4oLrw7tebnl3CWI9cpb2jaZl1cVrINhOnApukO1oqxOE2xGVCkNIZ4RIDLlj1WVbbLBN/C/dREjf6n/jNT5LylWEULHofYpit2+1o8KSHjT7aCMLwWlyIuAyYz45WSPviy4oyrjeLUGUo3u15EjJeGcKhXtrtreVdvk="
|
|
21
25
|
|
|
22
|
-
services:
|
|
23
|
-
- docker
|
|
24
|
-
|
|
25
|
-
# after_success:
|
|
26
|
-
# - CI=true TRAVIS=true coveralls --verbose
|
|
27
|
-
|
|
28
26
|
deploy:
|
|
29
27
|
provider: rubygems
|
|
30
28
|
api_key:
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# ROM DynamoDB Adapter
|
|
2
|
+
[](https://badge.fury.io/rb/rom-dynamodb) [](https://github.com/davidkelley/rom-dynamodb) [](http://www.rubydoc.info/github/davidkelley/rom-dynamodb) [](#license) [](https://gitter.im/rom-rb/chat)
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
[](https://codeclimate.com/github/davidkelley/rom-dynamodb) [](https://coveralls.io/github/davidkelley/rom-dynamodb?branch=master) [](https://gemnasium.com/github.com/davidkelley/rom-dynamodb)
|
|
4
5
|
[](https://travis-ci.org/davidkelley/rom-dynamodb) [](http://inch-ci.org/github/davidkelley/rom-dynamodb)
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
This adapter uses [ROM (>= 2.0.0)](http://rom-rb.org/) to provide an easy-to-use, clean and understandable interface for [AWS DynamoDB](https://aws.amazon.com/documentation/dynamodb/).
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
|
@@ -25,11 +26,150 @@ Or install it yourself as:
|
|
|
25
26
|
|
|
26
27
|
## Usage
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
The following container setup is for demonstration purposes only. You should follow the standard way of integrating ROM into your environment, as [documented here](http://rom-rb.org/learn/advanced/flat-style-setup/).
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'rom/dynamodb'
|
|
33
|
+
|
|
34
|
+
TABLE = "my-dynamodb-users-table"
|
|
35
|
+
|
|
36
|
+
# any other AWS::DynamoDB::Client options
|
|
37
|
+
credentials = { region: 'us-east-1' }
|
|
38
|
+
|
|
39
|
+
container = ROM.container(:dynamodb, credentials) do |rom|
|
|
40
|
+
rom.relation(:users) do
|
|
41
|
+
# Key Schema: id<Hash>
|
|
42
|
+
dataset TABLE
|
|
43
|
+
|
|
44
|
+
def by_id(val)
|
|
45
|
+
where { id == val }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
rom.commands(:users) do
|
|
50
|
+
FILTER = Functions[:symbolize_keys] >> Functions[:accept_keys, [:id]]
|
|
51
|
+
|
|
52
|
+
define(:create) do
|
|
53
|
+
KEYS = %w(id name)
|
|
54
|
+
result :one
|
|
55
|
+
input Functions[:accept_keys, KEYS]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
define(:delete) do
|
|
59
|
+
result :one
|
|
60
|
+
input FILTER
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
define(:update) do
|
|
64
|
+
result :one
|
|
65
|
+
input FILTER
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
relation = container.relation(:users)
|
|
71
|
+
|
|
72
|
+
relation.count # => 1234
|
|
73
|
+
|
|
74
|
+
relation.where { id == 1 }.one! # => { id: 1, name: "David" }
|
|
75
|
+
|
|
76
|
+
relation.info # => <Hash> DynamoDB Table Information
|
|
77
|
+
|
|
78
|
+
relation.status # => :active
|
|
79
|
+
|
|
80
|
+
# create a new user
|
|
81
|
+
create = container.commands[:users][:create]
|
|
82
|
+
user = create.call({ id: 2, name: "James" })
|
|
83
|
+
|
|
84
|
+
# update an existing user
|
|
85
|
+
update = container.commands[:users][:update]
|
|
86
|
+
update.by_id(user[:id]).call(name: "Mark")
|
|
87
|
+
|
|
88
|
+
relation.where(id: user[:id]) { id == id }.one! # => { id: 2, name: "Mark" }
|
|
89
|
+
|
|
90
|
+
# delete an existing user
|
|
91
|
+
delete = container.commands[:users][:delete]
|
|
92
|
+
delete.by_id(user[:id]).call
|
|
93
|
+
```
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
#### Querying a composite key DynamoDB Table
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
container = ROM.container(:dynamodb, credentials) do |rom|
|
|
100
|
+
rom.relation(:logs) do
|
|
101
|
+
# Key Schema: host<Hash>, timestamp<Range>
|
|
102
|
+
dataset "my-logs-table"
|
|
103
|
+
|
|
104
|
+
def by_host(ip)
|
|
105
|
+
where { host == ip }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def after_timestamp(time)
|
|
109
|
+
where { timestamp > time }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def before_timestamp(time)
|
|
113
|
+
where { timestamp < time }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
rom.commands(:logs) do
|
|
118
|
+
define(:create) do
|
|
119
|
+
KEYS = %w(host timestamp message)
|
|
120
|
+
result :one
|
|
121
|
+
input Functions[:accept_keys, KEYS]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
num_of_logs = 20
|
|
127
|
+
|
|
128
|
+
host = "192.168.0.1"
|
|
129
|
+
|
|
130
|
+
logs = (1..num_of_logs).to_a.collect do |i|
|
|
131
|
+
{ host: host, timestamp: Time.now.to_f + (i * 60), message: "some message" }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# create fake logs
|
|
135
|
+
container.commands[:logs][:create].call(logs)
|
|
136
|
+
|
|
137
|
+
relation = container.relation(:logs)
|
|
138
|
+
|
|
139
|
+
relation.count == num_of_logs # => true
|
|
140
|
+
|
|
141
|
+
all = relation.where(ip: host) { host == ip }.after(0).to_a # => [{host: "192.168.0.1", ... }, ...]
|
|
142
|
+
|
|
143
|
+
all.size # => 20
|
|
144
|
+
|
|
145
|
+
before = relation.where(ip: host) { [host == ip, timestamp < (Time.now.to_f + 60 * 60)] }.limit(1).to_a
|
|
146
|
+
|
|
147
|
+
before.size # => 1
|
|
148
|
+
|
|
149
|
+
before.first == logs.first # => true
|
|
150
|
+
|
|
151
|
+
offset = { ip: host, timestamp: logs[-2][:timestamp] }
|
|
152
|
+
|
|
153
|
+
last = relation.where(ip: host) { ip == host }.descending.after(0).offset(offset).limit(1).one!
|
|
154
|
+
|
|
155
|
+
last == logs.last # => true
|
|
156
|
+
```
|
|
157
|
+
---
|
|
29
158
|
|
|
30
159
|
## Development
|
|
31
160
|
|
|
32
|
-
|
|
161
|
+
All development takes place inside [Docker Compose](). Run the following commands to get setup:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
$ docker-compose pull
|
|
165
|
+
$ docker-compose build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
You can then begin developing, running RSpec tests with the following command:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
$ docker-compose run rom rspec [args...]
|
|
172
|
+
```
|
|
33
173
|
|
|
34
174
|
## Contributing
|
|
35
175
|
|
data/docker-compose.yml
CHANGED
|
@@ -13,7 +13,13 @@ services:
|
|
|
13
13
|
dockerfile: Dockerfile.development
|
|
14
14
|
command: rspec
|
|
15
15
|
environment:
|
|
16
|
-
DYNAMO_ENDPOINT
|
|
16
|
+
- DYNAMO_ENDPOINT=http://dynamo:8000
|
|
17
|
+
- CI
|
|
18
|
+
- TRAVIS
|
|
19
|
+
- TRAVIS_BRANCH
|
|
20
|
+
- TRAVIS_JOB_ID
|
|
21
|
+
- TRAVIS_PULL_REQUEST
|
|
22
|
+
- CODECLIMATE_REPO_TOKEN
|
|
17
23
|
volumes:
|
|
18
24
|
- .:/app
|
|
19
25
|
links:
|
data/lib/rom/dynamodb/dataset.rb
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
require "rom/dynamodb/dataset/where_clause"
|
|
2
|
+
|
|
1
3
|
module ROM
|
|
2
4
|
module DynamoDB
|
|
3
5
|
class Dataset
|
|
6
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
7
|
+
# @note This method can be chained with other relational methods.
|
|
8
|
+
|
|
9
|
+
# @!macro rom.dynamodb.dataset.order_note
|
|
10
|
+
# @note Items with the same partition key value are stored in sorted
|
|
11
|
+
# order by sort key. If the sort key data type is Number, the results
|
|
12
|
+
# are stored in numeric order. For type String, the results are stored
|
|
13
|
+
# in order of ASCII character code values. For type Binary, DynamoDB
|
|
14
|
+
# treats each byte of the binary data as unsigned.
|
|
15
|
+
|
|
16
|
+
# @!macro rom.dynamodb.dataset.series_note
|
|
17
|
+
# @note Whilst the naming and parameters of this method indicate a
|
|
18
|
+
# time-series based operation, DynamoDB can perform a range query
|
|
19
|
+
# for any orderable data.
|
|
20
|
+
|
|
21
|
+
# @attr_reader [String] name the full name of the DynamoDB Table to query.
|
|
22
|
+
|
|
23
|
+
# @attr_reader [Symbol] operation the operation to perform on DynamoDB.
|
|
24
|
+
|
|
25
|
+
# @attr_reader [Hash] config the configuration to apply to the underlying
|
|
26
|
+
# DynamoDB client.
|
|
27
|
+
|
|
4
28
|
attr_reader :name, :operation, :config
|
|
5
29
|
|
|
30
|
+
# @attr [Array<Hash>] queries the array of query sets to merge and use
|
|
31
|
+
# to query DynamoDB.
|
|
32
|
+
|
|
33
|
+
# This array of hashes is built up by chaining the
|
|
34
|
+
# relational methods together and when the query is finally executed,
|
|
35
|
+
# all the hashes in this array are merged and send to the underlying
|
|
36
|
+
# DynamoDB client.
|
|
37
|
+
|
|
6
38
|
attr_accessor :queries
|
|
7
39
|
|
|
8
40
|
def initialize(name:, operation: :query, config: {}, queries: [])
|
|
@@ -12,42 +44,211 @@ module ROM
|
|
|
12
44
|
@queries = queries
|
|
13
45
|
end
|
|
14
46
|
|
|
47
|
+
# The name of an index to query. This index can be any local secondary
|
|
48
|
+
# index or global secondary index on the table.
|
|
49
|
+
#
|
|
50
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
51
|
+
#
|
|
52
|
+
# @param name [String] the name of the index to query.
|
|
53
|
+
#
|
|
54
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
15
55
|
def index(name)
|
|
16
56
|
restrict(index_name: name)
|
|
17
57
|
end
|
|
18
58
|
|
|
59
|
+
# Performs a traversal of the index in ascending order on the range key.
|
|
60
|
+
# DynamoDB will return the results in the order in which they
|
|
61
|
+
# have been stored.
|
|
62
|
+
#
|
|
63
|
+
# @note This is the default behaviour.
|
|
64
|
+
#
|
|
65
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
66
|
+
# @!macro rom.dynamodb.dataset.order_note
|
|
67
|
+
#
|
|
68
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
19
69
|
def ascending
|
|
20
70
|
restrict(scan_index_forward: true)
|
|
21
71
|
end
|
|
22
72
|
|
|
73
|
+
# Performs a traversal of the index in descending order on the range key.
|
|
74
|
+
# DynamoDB reads the results in reverse order by sort key value, and
|
|
75
|
+
# then returns the results to the client.
|
|
76
|
+
#
|
|
77
|
+
# @note This is the default behaviour.
|
|
78
|
+
#
|
|
79
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
80
|
+
# @!macro rom.dynamodb.dataset.order_note
|
|
81
|
+
#
|
|
82
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
23
83
|
def descending
|
|
24
84
|
restrict(scan_index_forward: false)
|
|
25
85
|
end
|
|
26
86
|
|
|
87
|
+
# Limits the number of results returned by the query or scan operation.
|
|
88
|
+
#
|
|
89
|
+
# The maximum number of items to evaluate (not necessarily the number of
|
|
90
|
+
# matching items). If DynamoDB processes the number of items up to the
|
|
91
|
+
# limit while processing the results, it stops the operation and returns
|
|
92
|
+
# the matching values up to that point.
|
|
93
|
+
#
|
|
94
|
+
# If there are more matches to be returned, the {#last_evaluated_key}
|
|
95
|
+
#
|
|
96
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
97
|
+
#
|
|
98
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
27
99
|
def limit(num = nil)
|
|
28
100
|
append { { limit: num } unless num.nil? }
|
|
29
101
|
end
|
|
30
102
|
|
|
103
|
+
# The composite key of the first item that the resulting query will
|
|
104
|
+
# evaluate. Use this method if you have a populated {#last_evaluated_key}.
|
|
105
|
+
#
|
|
106
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
107
|
+
#
|
|
108
|
+
# @note When applying an offset, it must include the same composite key of
|
|
109
|
+
# which the index you are querying is composed from. Therefore, if your
|
|
110
|
+
# index has both a hash and range key, the key you provide must also have
|
|
111
|
+
# these.
|
|
112
|
+
#
|
|
113
|
+
# @param key [Hash] the composite offset key matching the queryable index.
|
|
114
|
+
#
|
|
115
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
31
116
|
def offset(key)
|
|
32
117
|
append { { exclusive_start_key: key } unless key.nil? }
|
|
33
118
|
end
|
|
34
119
|
|
|
120
|
+
# Selects one or more keys to retrieve from the table.
|
|
121
|
+
#
|
|
122
|
+
# These keys can include scalars, sets, or elements of a JSON
|
|
123
|
+
# document.
|
|
124
|
+
#
|
|
125
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
126
|
+
#
|
|
127
|
+
# @param keys [Array<String>] an array of string expressions to apply
|
|
128
|
+
# to the query
|
|
129
|
+
#
|
|
130
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
35
131
|
def select(keys)
|
|
36
|
-
restrict(
|
|
132
|
+
restrict(projection_expression: keys.collect(&:to_s).join(","))
|
|
37
133
|
end
|
|
38
134
|
|
|
135
|
+
# Restricts keys within a composite key, by values using a specific operand.
|
|
136
|
+
#
|
|
137
|
+
# You can compose where clauses using compartive operators from inside
|
|
138
|
+
# a block, allowing a greater level of flexibility.
|
|
139
|
+
#
|
|
140
|
+
# Multiple where clauses can be chained, or multiple predicates defined
|
|
141
|
+
# inside an array within a single clause. See the examples below.
|
|
142
|
+
#
|
|
143
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
144
|
+
#
|
|
145
|
+
# @note The following operands are supported, :>=, :>, :<, :<=, :== and :between
|
|
146
|
+
#
|
|
147
|
+
# @example Given Table[id<Hash>,legs<Range>]
|
|
148
|
+
# animals = relation.where { [id == "mammal", legs > 0] }
|
|
149
|
+
# animals #=> [{id: "mammal", legs: 2, name: "Human"}, ...]
|
|
150
|
+
#
|
|
151
|
+
# @example Using a value mapping hash
|
|
152
|
+
# keys = { type: "mammal", min_legs: 2 }
|
|
153
|
+
# animals = relation.where(keys) { [id == type, legs > min_legs] }
|
|
154
|
+
# animals #=> [{id: "mammal", legs: 2, name: "Elephant"}, ...]
|
|
155
|
+
#
|
|
156
|
+
# @example Matching by exact value
|
|
157
|
+
# keys = { type: "mammal" }
|
|
158
|
+
# animals = relation.where(keys) { id == type }
|
|
159
|
+
# animals #=> [{id: "mammal", legs: 2, name: "Elephant"}, ...]
|
|
160
|
+
#
|
|
161
|
+
# @example Between two values
|
|
162
|
+
#
|
|
163
|
+
# @example Matching with begins_with
|
|
164
|
+
#
|
|
165
|
+
def where(maps = {}, &block)
|
|
166
|
+
clauses = WhereClause.new(maps).execute(&block).clauses
|
|
167
|
+
append(:query) do
|
|
168
|
+
{
|
|
169
|
+
expression_attribute_values: clauses.expression_attribute_values,
|
|
170
|
+
expression_attribute_names: clauses.expression_attribute_names,
|
|
171
|
+
key_condition_expression: clauses.key_condition_expression,
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Retrieves a key present within the composite key for this index by its
|
|
177
|
+
# exact value.
|
|
178
|
+
#
|
|
179
|
+
# @deprecated Use {#where} instead.
|
|
180
|
+
#
|
|
181
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
182
|
+
#
|
|
183
|
+
# @example Given Table[id<Hash>]
|
|
184
|
+
# relation.equal(:id, 1).one! #=> { id: 1, ... }
|
|
185
|
+
#
|
|
186
|
+
# @example Given Table[id<Hash>,created_at<Range>]
|
|
187
|
+
# relation.equal(:id, 1).equal(:created_at, Time.now.to_f).one! #=> { id: 1, ... }
|
|
188
|
+
#
|
|
189
|
+
# @param key [Symbol] the key to match the provided value against.
|
|
190
|
+
# @param val the value to match against the key in the index.
|
|
191
|
+
# @param predicate [Symbol] the query predicate to apply to DynamoDB.
|
|
192
|
+
#
|
|
193
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
39
194
|
def equal(key, val, predicate = :eq)
|
|
40
195
|
restrict_by(key, predicate, [val])
|
|
41
196
|
end
|
|
42
197
|
|
|
198
|
+
# Retrieves all matching range keys within the composite key for the
|
|
199
|
+
# index between two points.
|
|
200
|
+
#
|
|
201
|
+
# @deprecated Use {#where} instead.
|
|
202
|
+
#
|
|
203
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
204
|
+
#
|
|
205
|
+
# @!macro rom.dynamodb.dataset.series_note
|
|
206
|
+
#
|
|
207
|
+
# @example Given Table[id<Hash>,legs<Range>]
|
|
208
|
+
# users = relation.equal(:id, "mammal").between(:legs, 0, 4).to_a
|
|
209
|
+
# users #=> [{id: "mammal", legs: 2, name: "Human"}, {id: "mammal", legs: 4, name: "Elephant"}, ...]
|
|
210
|
+
#
|
|
211
|
+
# @param key [Symbol] the key to match the provided value against.
|
|
212
|
+
# @param after the value to match range values after
|
|
213
|
+
# @param before the value to match range values before
|
|
214
|
+
#
|
|
215
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
43
216
|
def between(key, after, before, predicate = :between)
|
|
44
217
|
restrict_by(key, predicate, [after, before])
|
|
45
218
|
end
|
|
46
219
|
|
|
220
|
+
# Retrieves all matching range keys within the composite key after the
|
|
221
|
+
# value provided.
|
|
222
|
+
#
|
|
223
|
+
# @deprecated Use {#where} instead.
|
|
224
|
+
#
|
|
225
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
226
|
+
#
|
|
227
|
+
# @!macro rom.dynamodb.dataset.series_note
|
|
228
|
+
#
|
|
229
|
+
# @deprecated Use {#where} instead.
|
|
230
|
+
#
|
|
231
|
+
# @example Given Table[id<Hash>,legs<Range>]
|
|
232
|
+
# users = relation.equal(:id, "mammal").after(:legs, 0).to_a
|
|
233
|
+
# users #=> [{id: "mammal", legs: 2, name: "Human"}, ...]
|
|
234
|
+
#
|
|
235
|
+
# @param key [Symbol] the key to match the provided value against.
|
|
236
|
+
# @param after the value to match range values after
|
|
237
|
+
# @param predicate [String] the query predicate to apply to DynamoDB.
|
|
238
|
+
#
|
|
239
|
+
# @return [self] the {Dataset} object the method was performed on.
|
|
47
240
|
def after(key, after, predicate = :ge)
|
|
48
241
|
restrict_by(key, predicate, [after])
|
|
49
242
|
end
|
|
50
243
|
|
|
244
|
+
# Retreives all matching range keys within the composite key before the
|
|
245
|
+
# value provided.
|
|
246
|
+
#
|
|
247
|
+
# @deprecated Use {#where} instead.
|
|
248
|
+
#
|
|
249
|
+
# @!macro rom.dynamodb.dataset.chain_note
|
|
250
|
+
#
|
|
251
|
+
# @!macro rom.dynamodb.dataset.series_note
|
|
51
252
|
def before(key, before, predicate = :le)
|
|
52
253
|
restrict_by(key, predicate, [before])
|
|
53
254
|
end
|
|
@@ -135,6 +336,9 @@ module ROM
|
|
|
135
336
|
|
|
136
337
|
def append(operation = :query, &block)
|
|
137
338
|
result = block.call
|
|
339
|
+
if result[:key_condition_expression]
|
|
340
|
+
|
|
341
|
+
end
|
|
138
342
|
if result
|
|
139
343
|
args = { name: name, config: config, operation: operation }
|
|
140
344
|
self.class.new(args.merge(queries: queries + [result].flatten))
|
|
@@ -144,17 +348,10 @@ module ROM
|
|
|
144
348
|
end
|
|
145
349
|
|
|
146
350
|
def to_update_structure(hash)
|
|
147
|
-
values = {}
|
|
148
|
-
maps = {}
|
|
149
|
-
expr = 'SET ' + hash.map do |key, val|
|
|
150
|
-
values[":#{key}"] = val
|
|
151
|
-
maps["##{key}"] = key
|
|
152
|
-
"##{key}=:#{key}"
|
|
153
|
-
end.join(', ')
|
|
154
351
|
{
|
|
155
|
-
expression_attribute_values:
|
|
156
|
-
expression_attribute_names:
|
|
157
|
-
update_expression:
|
|
352
|
+
expression_attribute_values: Hash[hash.map { |k, v| [":#{k}_u", v] }],
|
|
353
|
+
expression_attribute_names: Hash[hash.map { |k, v| ["##{k}_u", k] }],
|
|
354
|
+
update_expression: "SET " + hash.map { |k, v| "##{k}_u = :#{k}_u" }.join(", "),
|
|
158
355
|
}
|
|
159
356
|
end
|
|
160
357
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module ROM
|
|
2
|
+
module DynamoDB
|
|
3
|
+
class Dataset
|
|
4
|
+
class WhereClause
|
|
5
|
+
class Clause
|
|
6
|
+
attr_reader :clauses
|
|
7
|
+
|
|
8
|
+
def initialize(clauses = [])
|
|
9
|
+
@clauses = clauses.is_a?(Array) ? clauses : [clauses]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def concat(val)
|
|
13
|
+
@clauses.concat(val.is_a?(Array) ? val : [val])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def expression_attribute_values
|
|
17
|
+
clauses.collect(&method(:to_values)).inject(:merge)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def expression_attribute_names
|
|
21
|
+
clauses.collect(&method(:to_names)).inject(:merge)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def key_condition_expression
|
|
25
|
+
clauses.collect(&method(:to_expression)).join(" AND ")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_expression(clause)
|
|
29
|
+
case clause.operand
|
|
30
|
+
when :between
|
|
31
|
+
"##{clause.key} BETWEEN :#{clause.key}_a AND :#{clause.key}_b"
|
|
32
|
+
when :begins_with
|
|
33
|
+
"begins_with(##{clause.key}, :#{clause.key})"
|
|
34
|
+
when :==
|
|
35
|
+
"##{clause.key} = :#{clause.key}"
|
|
36
|
+
else
|
|
37
|
+
"##{clause.key} #{clause.operand} :#{clause.key}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_values(clause)
|
|
42
|
+
case clause.operand
|
|
43
|
+
when :between
|
|
44
|
+
value = clause.val
|
|
45
|
+
min = value.min.is_a?(Operand) ? value.min.val : value.min
|
|
46
|
+
max = value.max.is_a?(Operand) ? value.max.val : value.max
|
|
47
|
+
{
|
|
48
|
+
":#{clause.key}_a" => min,
|
|
49
|
+
":#{clause.key}_b" => max
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
{ ":#{clause.key}" => clause.val }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_names(clause)
|
|
57
|
+
{ "##{clause.key}" => clause.key }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Operand
|
|
62
|
+
attr_reader :key, :operand, :val
|
|
63
|
+
|
|
64
|
+
def initialize(key:, val:)
|
|
65
|
+
@key = key
|
|
66
|
+
@val = val
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
%i(<= == >= > < between).each do |com|
|
|
70
|
+
define_method(com) do |val|
|
|
71
|
+
@operand = com
|
|
72
|
+
@val = val.is_a?(Operand) ? val.val : val
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def <=>(op)
|
|
78
|
+
val <=> op.val
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
attr_reader :maps, :clauses
|
|
83
|
+
|
|
84
|
+
def initialize(maps = {})
|
|
85
|
+
@clauses = Clause.new
|
|
86
|
+
@maps = maps
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def execute(&block)
|
|
90
|
+
@clauses.concat(instance_exec(&block))
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def method_missing(key)
|
|
95
|
+
Operand.new(key: key, val: maps[key])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -3,29 +3,72 @@ module ROM
|
|
|
3
3
|
class Relation < ROM::Relation
|
|
4
4
|
adapter :dynamodb
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# @!macro [attach] r.forward
|
|
7
|
+
# Performs a $1 action on the relation. In most cases, these operations
|
|
8
|
+
# can be chained to build up larger queries to perform.
|
|
9
|
+
# @note This method forwards the $1 operation to the underlying dataset.
|
|
10
|
+
# @see DynamoDB::Dataset#$1
|
|
11
|
+
forward :restrict
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
forward :create, :delete, :update
|
|
13
|
+
forward :scan
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
forward :ascending, :descending
|
|
15
|
+
forward :retrieve
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
# super *r
|
|
18
|
-
# puts schema.inspect
|
|
19
|
-
# end
|
|
17
|
+
forward :batch_get
|
|
20
18
|
|
|
19
|
+
forward :equal
|
|
20
|
+
|
|
21
|
+
forward :index
|
|
22
|
+
|
|
23
|
+
forward :where
|
|
24
|
+
|
|
25
|
+
forward :before
|
|
26
|
+
|
|
27
|
+
forward :after
|
|
28
|
+
|
|
29
|
+
forward :between
|
|
30
|
+
|
|
31
|
+
forward :select
|
|
32
|
+
|
|
33
|
+
forward :offset
|
|
34
|
+
|
|
35
|
+
forward :limit
|
|
36
|
+
|
|
37
|
+
forward :create
|
|
38
|
+
|
|
39
|
+
forward :delete
|
|
40
|
+
|
|
41
|
+
forward :update
|
|
42
|
+
|
|
43
|
+
forward :ascending
|
|
44
|
+
|
|
45
|
+
forward :descending
|
|
46
|
+
|
|
47
|
+
# Retrieve a single record, providing a hash key name and the ID to
|
|
48
|
+
# fetch.
|
|
49
|
+
#
|
|
50
|
+
# @note This is a very simple helper allowing you to easily retrieve
|
|
51
|
+
# singular records using a hash key lookup.
|
|
52
|
+
#
|
|
53
|
+
# @param key [Symbol] the hash key name to fetch on
|
|
54
|
+
# @param id [String, Fixnum] an accepted data format for DynamoDB to lookup on
|
|
55
|
+
# @return [Hash] a single object retrieved from DynamoDB
|
|
56
|
+
def fetch(key, id)
|
|
57
|
+
retrieve(key, id).one!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @see https://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method
|
|
61
|
+
# @return [Hash] AWS SDK payload of table information
|
|
21
62
|
def info
|
|
22
63
|
dataset.information
|
|
23
64
|
end
|
|
24
65
|
|
|
66
|
+
# @return [Fixnum] current total item count for the associated DynamoDB table
|
|
25
67
|
def count
|
|
26
68
|
dataset.information.item_count
|
|
27
69
|
end
|
|
28
70
|
|
|
71
|
+
# @return [Symbol] current status of the DynamoDB table
|
|
29
72
|
def status
|
|
30
73
|
dataset.information.table_status.downcase.to_sym
|
|
31
74
|
rescue
|
data/lib/rom/dynamodb/version.rb
CHANGED
data/rom-dynamodb.gemspec
CHANGED
|
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_development_dependency "bigdecimal", "~> 1.3"
|
|
29
29
|
spec.add_development_dependency "rake", "~> 10.0"
|
|
30
30
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
31
|
-
spec.add_development_dependency "
|
|
31
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
|
32
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 1.0"
|
|
32
33
|
spec.add_development_dependency "factory_girl", "~> 4.5"
|
|
33
34
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rom-dynamodb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- davidkelley
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-01
|
|
11
|
+
date: 2017-02-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rom
|
|
@@ -123,19 +123,33 @@ dependencies:
|
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '3.0'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
126
|
+
name: yard
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '0.
|
|
131
|
+
version: '0.9'
|
|
132
132
|
type: :development
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '0.
|
|
138
|
+
version: '0.9'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: codeclimate-test-reporter
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '1.0'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '1.0'
|
|
139
153
|
- !ruby/object:Gem::Dependency
|
|
140
154
|
name: factory_girl
|
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,6 +176,7 @@ files:
|
|
|
162
176
|
- ".gitignore"
|
|
163
177
|
- ".rspec"
|
|
164
178
|
- ".travis.yml"
|
|
179
|
+
- CHANGELOG.md
|
|
165
180
|
- CODE_OF_CONDUCT.md
|
|
166
181
|
- Dockerfile.development
|
|
167
182
|
- Gemfile
|
|
@@ -177,6 +192,7 @@ files:
|
|
|
177
192
|
- lib/rom/dynamodb/commands/delete.rb
|
|
178
193
|
- lib/rom/dynamodb/commands/update.rb
|
|
179
194
|
- lib/rom/dynamodb/dataset.rb
|
|
195
|
+
- lib/rom/dynamodb/dataset/where_clause.rb
|
|
180
196
|
- lib/rom/dynamodb/functions.rb
|
|
181
197
|
- lib/rom/dynamodb/gateway.rb
|
|
182
198
|
- lib/rom/dynamodb/relation.rb
|