dynamodb_record 0.0.1
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 +7 -0
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +30 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/dynamodb_record.gemspec +32 -0
- data/lib/dynamodb_record/associations.rb +59 -0
- data/lib/dynamodb_record/collection.rb +47 -0
- data/lib/dynamodb_record/config.rb +16 -0
- data/lib/dynamodb_record/document.rb +45 -0
- data/lib/dynamodb_record/fields.rb +131 -0
- data/lib/dynamodb_record/finders.rb +21 -0
- data/lib/dynamodb_record/persistence.rb +127 -0
- data/lib/dynamodb_record/query.rb +50 -0
- data/lib/dynamodb_record/version.rb +5 -0
- data/lib/dynamodb_record.rb +27 -0
- data/spec/app/models/person.rb +7 -0
- data/spec/app/models/user.rb +5 -0
- data/spec/dynamodb_record/document_spec.rb +25 -0
- data/spec/dynamodb_record/fields_spec.rb +101 -0
- data/spec/dynamodb_record/finders_spec.rb +19 -0
- data/spec/fixtures/vcr_cassettes/DynamodbRecord_Document/initializes_from_database.yml +54 -0
- data/spec/fixtures/vcr_cassettes/DynamodbRecord_Fields/_find/finds_record.yml +54 -0
- data/spec/fixtures/vcr_cassettes/DynamodbRecord_Fields/_find/when_record_doesn_t_exists/returns_empty_object.yml +54 -0
- data/spec/spec_helper.rb +18 -0
- metadata +225 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bafe4b334f021f466ddd622e5568d05e0b01a3b5524e7ab144b702d8370fbd58
|
4
|
+
data.tar.gz: 9eab54f02aee3d337c9e714843d397e9de8caf094a0e458964c2aece056bee85
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f60d3f15b9abf2a73411c6482c4fe0fefa42c386cf61190419d43cbd971b487d8c7d8d0fdd35e710fb3c9229e2e3a5515fd0f41cca682ad30c6d08801e5a460
|
7
|
+
data.tar.gz: cf7ecd56ff1bd2865c9acefa52c3340ceb7ac3d672a005070b5838f8295564e89e7babc8bf03074c01563396148e92cf975de2e34803d3e9afb85893fc7cd180
|
@@ -0,0 +1,44 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
jobs:
|
8
|
+
test:
|
9
|
+
runs-on: ubuntu-22.04
|
10
|
+
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v4
|
13
|
+
- uses: actions/setup-node@v4
|
14
|
+
- uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: 3.2.3
|
17
|
+
|
18
|
+
- name: Install dependencies
|
19
|
+
run: |
|
20
|
+
gem install rspec -v 3.7.0
|
21
|
+
gem install rubocop
|
22
|
+
gem install vcr -v 6.2
|
23
|
+
gem install webmock -v 3.23
|
24
|
+
gem install rexml -v 3.2.6
|
25
|
+
gem install jwt -v 2.8.1
|
26
|
+
gem install aws-sdk-sqs -v 1.70.0
|
27
|
+
gem install aws-sdk-dynamodb -v 1.105.0
|
28
|
+
gem install activesupport -v 7.1.3.2
|
29
|
+
|
30
|
+
- name: Run tests
|
31
|
+
run: bundle exec rspec
|
32
|
+
|
33
|
+
# - name: Bundle install
|
34
|
+
# run: |
|
35
|
+
# bundle config path vendor/bundle
|
36
|
+
# bundle install --jobs 4 --retry 3
|
37
|
+
|
38
|
+
# - run: bundle exec rake
|
39
|
+
# - name: Install dependencies
|
40
|
+
# run: bundle install
|
41
|
+
#
|
42
|
+
# - name: Run RuboCop
|
43
|
+
# run: bundle exec rubocop
|
44
|
+
#
|
data/.gitignore
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/Gemfile.lock
|
4
|
+
/_yardoc/
|
5
|
+
/coverage/
|
6
|
+
/doc/
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/tmp/
|
10
|
+
*.bundle
|
11
|
+
*.so
|
12
|
+
*.o
|
13
|
+
*.a
|
14
|
+
*.gem
|
15
|
+
mkmf.log
|
16
|
+
.env
|
17
|
+
.DS_Store
|
18
|
+
.idea/*
|
19
|
+
log/*
|
20
|
+
measurement/*
|
21
|
+
pkg/*
|
22
|
+
*~
|
23
|
+
.rvmrc
|
24
|
+
.bundle
|
25
|
+
demo.rb
|
26
|
+
coverage/*
|
27
|
+
spec/dummy/tmp/*
|
28
|
+
spec/dummy/log/*.log
|
29
|
+
vendor/bundle
|
30
|
+
vendor
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.3
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Cars Ok
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# DynamoRecord
|
2
|
+
|
3
|
+
A simple DynamoDB ORM container on aws-sdk v3 forked from https://github.com/yetanothernguyen/dynamo_record
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'dynamo_record'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install dynamo_record
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Models
|
24
|
+
|
25
|
+
To create a model with DynamoRecord, simply include the DynamoRecord::Document mixin in your class as such::
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class User
|
29
|
+
include DynamoRecord::Document
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### Fields
|
34
|
+
Declaring a field is done by using the `field` method. For example, the following defines a User model with a first and last name:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class User
|
38
|
+
include DynamoRecord::Document
|
39
|
+
|
40
|
+
field :first_name, :string
|
41
|
+
field :last_name, :string
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
`field` accepts the following options:
|
46
|
+
- :default to specify a default value
|
47
|
+
- :hash_key to specify a DynamoDB hash key
|
48
|
+
- :range_key to specify a DynamoDB range key
|
49
|
+
- :index to specify an index
|
50
|
+
|
51
|
+
### Persistence
|
52
|
+
DynamoRecord provides a similar persistence interface compared to other ORMs.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
user = User.new(first_name: 'John', last_name: 'Doe')
|
56
|
+
user.save
|
57
|
+
|
58
|
+
user = User.create(first_name: 'John', last_name: 'Doe')
|
59
|
+
|
60
|
+
user.destroy
|
61
|
+
```
|
62
|
+
|
63
|
+
### Querying
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
users = User.all
|
67
|
+
|
68
|
+
users = User.where(first_name: 'John')
|
69
|
+
|
70
|
+
users = User.where(first_name: 'John', limit: 5)
|
71
|
+
|
72
|
+
user = User.find('f9b351b0-d06d-4fff-b8d4-8af162e2b8ba')
|
73
|
+
```
|
74
|
+
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
1. Fork it ( https://github.com/[my-github-username]/dynamo_record/fork )
|
79
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
80
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
81
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
5. Create a new Pull Request
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'dynamodb_record/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'dynamodb_record'
|
9
|
+
spec.version = DynamodbRecord::VERSION
|
10
|
+
spec.author = ['Henry Guzman', 'Jhon Santander']
|
11
|
+
spec.email = ['hguzman10@gmail.com', 'jsantander1219@gmail.com']
|
12
|
+
spec.summary = 'A simple DynamoDB ORM'
|
13
|
+
spec.homepage = 'https://github.com/CarsOk/dynamodb-record'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.2.0'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'activesupport', '~> 7.1', '>= 7.1.3.2'
|
23
|
+
spec.add_dependency 'aws-sdk-dynamodb', '~> 1.105'
|
24
|
+
spec.add_dependency 'aws-sdk-sqs', '~> 1.70'
|
25
|
+
spec.add_dependency 'jwt', '~> 2.8', '>= 2.8.1'
|
26
|
+
spec.add_dependency 'rexml', '~> 3.2', '>= 3.2.6'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.7'
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 1.62'
|
30
|
+
spec.add_development_dependency 'vcr', '~> 6.2'
|
31
|
+
spec.add_development_dependency 'webmock', '~> 3.23'
|
32
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
module Associations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def has_many(associations)
|
9
|
+
model = associations.to_s.upcase.chop
|
10
|
+
define_method(associations) do
|
11
|
+
# options = self.class.default_options
|
12
|
+
field = self.class.to_s.downcase
|
13
|
+
options = { table_name: associations }
|
14
|
+
options.merge!(key_condition_expression: "##{field}_id = :#{field}_id")
|
15
|
+
options.merge!(expression_attribute_names: { "##{field}_id": "#{field}_id" })
|
16
|
+
options.merge!(expression_attribute_values: { ":#{field}_id" => id })
|
17
|
+
|
18
|
+
options.merge!(index_name: "#{self.class.to_s.downcase}_id_index")
|
19
|
+
|
20
|
+
klass = Object.const_get(model.capitalize)
|
21
|
+
# puts options
|
22
|
+
response = self.class.client.query(options)
|
23
|
+
|
24
|
+
# pager = DynamodbRecord::Pager.new(self.class.client, options, clase)
|
25
|
+
DynamodbRecord::Collection.new(response, klass, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_and_belongs_to_many(associations)
|
30
|
+
base_model = to_s.downcase
|
31
|
+
relation_model = associations.to_s.chop
|
32
|
+
list = []
|
33
|
+
list << base_model
|
34
|
+
list << relation_model
|
35
|
+
sorted_list = list.sort
|
36
|
+
table = sorted_list.map(&:pluralize).join('_')
|
37
|
+
|
38
|
+
define_method(associations) do
|
39
|
+
options = { table_name: table }
|
40
|
+
|
41
|
+
if sorted_list.first == base_model
|
42
|
+
field = sorted_list.first
|
43
|
+
else
|
44
|
+
field = sorted_list.last
|
45
|
+
options.merge!(index_name: "#{table}_index")
|
46
|
+
end
|
47
|
+
options.merge!(key_condition_expression: "##{field}_id = :#{field}_id")
|
48
|
+
options.merge!(expression_attribute_names: { "##{field}_id": "#{field}_id" })
|
49
|
+
options.merge!(expression_attribute_values: { ":#{field}_id" => id })
|
50
|
+
|
51
|
+
klass = Object.const_get(relation_model.capitalize)
|
52
|
+
response = self.class.client.query(options)
|
53
|
+
|
54
|
+
DynamodbRecord::Collection.new(response, klass, options)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
class Collection
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# attr_reader :last_evaluated_key
|
8
|
+
|
9
|
+
def initialize(pager, klass, options = {})
|
10
|
+
@klass = klass
|
11
|
+
@table_name = options[:table_name]
|
12
|
+
@foreign_key = options[:expression_attribute_values].transform_keys { |k| k.delete_prefix(':').to_sym }
|
13
|
+
@items = pager.items.map { |item| klass.send(:from_database, item) }
|
14
|
+
@last_evaluated_key = pager.last_evaluated_key
|
15
|
+
end
|
16
|
+
|
17
|
+
def last_evaluated_key
|
18
|
+
if @last_evaluated_key.nil?
|
19
|
+
nil
|
20
|
+
else
|
21
|
+
json_string = JSON.dump(@last_evaluated_key)
|
22
|
+
Base64.urlsafe_encode64(json_string)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(&)
|
27
|
+
@items.each(&)
|
28
|
+
end
|
29
|
+
|
30
|
+
def next_page?
|
31
|
+
last_evaluated_key ? true : false
|
32
|
+
end
|
33
|
+
|
34
|
+
def create!(items = {})
|
35
|
+
items.merge!(@foreign_key)
|
36
|
+
object = @klass.create!(items)
|
37
|
+
unless @klass.to_s.pluralize.downcase == @table_name.to_s
|
38
|
+
|
39
|
+
client = @klass.client
|
40
|
+
@foreign_key.merge!({ "#{@klass.to_s.downcase}_id": object.id, created_at: Time.now.to_i })
|
41
|
+
client.put_item({ table_name: @table_name, item: @foreign_key })
|
42
|
+
end
|
43
|
+
@items << object
|
44
|
+
object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DynamodbRecord
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
attr_accessor :access_key_id, :secret_access_key, :region, :namespace, :endpoint, :read_capacity_units,
|
6
|
+
:write_capacity_units, :compute_checksums
|
7
|
+
|
8
|
+
def set_defaults
|
9
|
+
self.region = 'us-east-1'
|
10
|
+
self.read_capacity_units = 20
|
11
|
+
self.write_capacity_units = 20
|
12
|
+
self.namespace = nil
|
13
|
+
end
|
14
|
+
set_defaults
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
module Document
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include DynamodbRecord::Fields
|
7
|
+
include DynamodbRecord::Persistence
|
8
|
+
include DynamodbRecord::Finders
|
9
|
+
include DynamodbRecord::Query
|
10
|
+
include DynamodbRecord::Associations
|
11
|
+
|
12
|
+
included do
|
13
|
+
class_attribute :base_class
|
14
|
+
self.base_class = self
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def from_database(attrs)
|
19
|
+
new(attrs, true).tap { |r| r.new_record = false }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :new_record
|
24
|
+
|
25
|
+
def initialize(params = {}, ignore_unknown_field = false)
|
26
|
+
@new_record = true
|
27
|
+
@attributes = {} # Validar esta linea
|
28
|
+
|
29
|
+
# Set default
|
30
|
+
self.class.attributes.each do |key, value|
|
31
|
+
send("#{key}=", value[:options][:default]) if value[:options][:default]
|
32
|
+
end
|
33
|
+
|
34
|
+
load(params, ignore_unknown_field)
|
35
|
+
end
|
36
|
+
|
37
|
+
def load(params, ignore_unknown_field = false)
|
38
|
+
params.each do |key, value|
|
39
|
+
next if ignore_unknown_field && !respond_to?("#{key}=")
|
40
|
+
|
41
|
+
send("#{key}=", value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
module Fields
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :attributes, :hash_key, instance_writer: true
|
9
|
+
self.attributes = {}
|
10
|
+
self.hash_key = nil
|
11
|
+
|
12
|
+
# default hash key
|
13
|
+
field :id, :string
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def field(name, type = :string, opts = {})
|
18
|
+
named = name.to_s
|
19
|
+
# Add attributes
|
20
|
+
attributes.merge!(name => { type:, options: opts })
|
21
|
+
|
22
|
+
self.hash_key = name if opts[:hash_key]
|
23
|
+
|
24
|
+
# Generate methods to field
|
25
|
+
define_method("#{named}=") { |value| write_attribute(named, value) }
|
26
|
+
define_method(name.to_s) { read_attribute(named) }
|
27
|
+
define_method("#{name}?") do
|
28
|
+
value = read_attribute(named)
|
29
|
+
return value != 'false' if value.is_a?(String)
|
30
|
+
|
31
|
+
!!value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def undump_field(value, options)
|
36
|
+
return nil if options.nil?
|
37
|
+
|
38
|
+
case options[:type]
|
39
|
+
when :integer
|
40
|
+
value.to_i
|
41
|
+
when :string
|
42
|
+
value.to_s
|
43
|
+
when :big_decimal
|
44
|
+
value.to_d
|
45
|
+
when :boolean
|
46
|
+
if ['true', true].include?(value)
|
47
|
+
true
|
48
|
+
elsif ['false', false].include?(value)
|
49
|
+
false
|
50
|
+
else
|
51
|
+
raise ArgumentError, 'Boolean column neither true nor false'
|
52
|
+
end
|
53
|
+
when :datetime
|
54
|
+
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
55
|
+
value
|
56
|
+
else
|
57
|
+
DateTime.parse(value)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def dump_field(value, options)
|
65
|
+
return value if options.nil?
|
66
|
+
|
67
|
+
case options[:type]
|
68
|
+
when :datetime
|
69
|
+
value.iso8601
|
70
|
+
else
|
71
|
+
value # aws-sdk supports the rest of data Types
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def unload(attrs)
|
76
|
+
{}.tap do |hash|
|
77
|
+
attrs.each do |key, value|
|
78
|
+
if attributes[key.to_sym][:options][:hash_key]
|
79
|
+
# puts "KEY #{key} | #{dump_field(value, self.attributes[key.to_sym])}"
|
80
|
+
hash[:pk] = dump_field(value, attributes[key.to_sym])
|
81
|
+
end
|
82
|
+
|
83
|
+
# puts "KEY #{key}|#{value}|#{self.attributes[key.to_sym]}"
|
84
|
+
hash[key] = dump_field(value, attributes[key.to_sym])
|
85
|
+
# puts "HASH: #{hash}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash_key
|
91
|
+
:id # default hash key
|
92
|
+
end
|
93
|
+
|
94
|
+
def range_key
|
95
|
+
@range_key ||= begin
|
96
|
+
attributes.select { |_k, v| v[:options][:range_key] }.keys.first
|
97
|
+
rescue StandardError
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def secondary_indexes
|
103
|
+
@secondary_indexes ||= begin
|
104
|
+
attributes.select { |_k, v| v[:options][:index] }.keys
|
105
|
+
rescue StandardError
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def dynamodb_type(type)
|
111
|
+
case type
|
112
|
+
when :integer, :big_decimal
|
113
|
+
'N'
|
114
|
+
when :string, :datetime
|
115
|
+
'S'
|
116
|
+
# else
|
117
|
+
# 'S'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Return a value
|
123
|
+
def write_attribute(name, value)
|
124
|
+
attributes[name.to_sym] = self.class.undump_field(value, self.class.attributes[name.to_sym])
|
125
|
+
end
|
126
|
+
|
127
|
+
def read_attribute(name)
|
128
|
+
attributes[name.to_sym]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
module Finders
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def find(id, range_key = nil)
|
9
|
+
key = { 'id' => id }
|
10
|
+
|
11
|
+
key[self.range_key] = range_key if self.range_key
|
12
|
+
puts table_name
|
13
|
+
response = client.get_item(
|
14
|
+
table_name:,
|
15
|
+
key:
|
16
|
+
)
|
17
|
+
response.item ? from_database(response.item) : nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DynamodbRecord
|
4
|
+
# Persistence Module
|
5
|
+
module Persistence
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def table_name
|
10
|
+
name = ActiveSupport::Inflector.tableize(base_class.name)
|
11
|
+
@table_name ||= DynamodbRecord::Config.namespace ? "#{DynamodbRecord::Config.namespace}-#{name}" : name
|
12
|
+
end
|
13
|
+
|
14
|
+
def client
|
15
|
+
opts = { region: DynamodbRecord::Config.region }
|
16
|
+
opts[:endpoint] = DynamodbRecord::Config.endpoint if DynamodbRecord::Config.endpoint
|
17
|
+
opts[:access_key_id] = DynamodbRecord::Config.access_key_id if DynamodbRecord::Config.access_key_id
|
18
|
+
opts[:secret_access_key] = DynamodbRecord::Config.access_key_id if DynamodbRecord::Config.secret_access_key
|
19
|
+
@client ||= Aws::DynamoDB::Client.new(opts)
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_options
|
23
|
+
{ table_name: }
|
24
|
+
end
|
25
|
+
|
26
|
+
def describe_table
|
27
|
+
client.describe_table(default_options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_table(opts = {})
|
31
|
+
table_name = opts[:table_name] || self.table_name
|
32
|
+
read_capacity = opts[:read_capacity] || DynamodbRecord::Config.read_capacity_units
|
33
|
+
write_capacity = opts[:write_capacity] || DynamodbRecord::Config.write_capacity_units
|
34
|
+
|
35
|
+
attribute_definitions = []
|
36
|
+
key_schema = []
|
37
|
+
|
38
|
+
# Default id hash key
|
39
|
+
attribute_definitions << { attribute_name: 'id',
|
40
|
+
attribute_type: 'S' }
|
41
|
+
key_schema << { attribute_name: 'id',
|
42
|
+
key_type: 'HASH' }
|
43
|
+
|
44
|
+
if range_key
|
45
|
+
attribute_definitions << { attribute_name: range_key.to_s,
|
46
|
+
attribute_type: dynamodb_type(attributes[range_key][:type]) }
|
47
|
+
key_schema << { attribute_name: range_key.to_s,
|
48
|
+
key_type: 'RANGE' }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Global secondary indexes
|
52
|
+
indexes = []
|
53
|
+
attributes.each do |key, value|
|
54
|
+
indexes << key if value[:options][:index]
|
55
|
+
end
|
56
|
+
|
57
|
+
global_secondary_indexes = []
|
58
|
+
indexes.each do |index|
|
59
|
+
index_definition = {}
|
60
|
+
index_definition[:index_name] = "#{index}_index"
|
61
|
+
index_definition[:key_schema] = [{ attribute_name: index, key_type: 'HASH' },
|
62
|
+
{ attribute_name: 'id', key_type: 'RANGE' }]
|
63
|
+
index_definition[:projection] = { projection_type: 'ALL' }
|
64
|
+
index_definition[:provisioned_throughput] = {
|
65
|
+
read_capacity_units: 1,
|
66
|
+
write_capacity_units: 1
|
67
|
+
}
|
68
|
+
global_secondary_indexes << index_definition
|
69
|
+
attribute_definitions << { attribute_name: index.to_s,
|
70
|
+
attribute_type: dynamodb_type(attributes[index][:type]) }
|
71
|
+
end
|
72
|
+
|
73
|
+
options = {
|
74
|
+
attribute_definitions:,
|
75
|
+
table_name:,
|
76
|
+
key_schema:,
|
77
|
+
provisioned_throughput: {
|
78
|
+
read_capacity_units: read_capacity,
|
79
|
+
write_capacity_units: write_capacity
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
options.merge!(global_secondary_indexes:) unless global_secondary_indexes.empty?
|
84
|
+
client.create_table(options)
|
85
|
+
end
|
86
|
+
|
87
|
+
def create(key = {})
|
88
|
+
create!(key)
|
89
|
+
rescue StandardError
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def create!(key = {})
|
94
|
+
object = new(key)
|
95
|
+
object.save!
|
96
|
+
object
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def save
|
101
|
+
save!
|
102
|
+
true
|
103
|
+
rescue StandardError
|
104
|
+
false
|
105
|
+
end
|
106
|
+
|
107
|
+
def save!
|
108
|
+
options = self.class.default_options
|
109
|
+
|
110
|
+
self.id = SecureRandom.uuid if id.nil?
|
111
|
+
|
112
|
+
if @new_record # New item. Don't overwrite if id exists
|
113
|
+
options.merge!(condition_expression: 'id <> :s', expression_attribute_values: { ':s' => id })
|
114
|
+
end
|
115
|
+
|
116
|
+
options.merge!(item: self.class.unload(attributes))
|
117
|
+
self.class.client.put_item(options)
|
118
|
+
@new_record = false
|
119
|
+
end
|
120
|
+
|
121
|
+
def destroy
|
122
|
+
options = self.class.default_options
|
123
|
+
key = { 'pk' => pk, 'sk' => sk }
|
124
|
+
self.class.client.delete_item(options.merge(key:))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|