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 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1 (10-Mar-2024)
4
+
5
+ * initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ # group :test do
8
+ # gem 'rspec', '~> 3.7'
9
+ # gem 'rubocop', '~> 1.62'
10
+ # end
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