activeitem 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.
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveItem
4
+ class Transaction
5
+ MAX_ITEMS = 100
6
+
7
+ attr_reader :operations
8
+
9
+ def initialize
10
+ @operations = []
11
+ end
12
+
13
+ def put(record, condition: nil)
14
+ record.instance_variable_set(:@id, SecureRandom.uuid) unless record.id
15
+ pk = record.class.primary_key
16
+ record.instance_variable_set(:"@#{pk}", record.id) if pk != 'id'
17
+ now = Time.now.utc.iso8601
18
+ record.instance_variable_set(:@created_at, now) unless record.created_at
19
+ record.instance_variable_set(:@updated_at, now)
20
+
21
+ item = record.send(:build_dynamodb_item).merge(
22
+ 'createdAt' => record.created_at,
23
+ 'updatedAt' => record.updated_at
24
+ )
25
+
26
+ op = { put: { table_name: record.class.table_name, item: item } }
27
+ op[:put][:condition_expression] = condition if condition
28
+
29
+ @operations << { op: op, record: record, type: :put }
30
+ end
31
+
32
+ def delete(record)
33
+ @operations << {
34
+ op: { delete: { table_name: record.class.table_name, key: { record.class.primary_key.to_s => record.id } } },
35
+ record: record, type: :delete
36
+ }
37
+ end
38
+
39
+ def update(record)
40
+ changes = record.changes
41
+ return if changes.empty?
42
+
43
+ set_parts = []
44
+ remove_parts = []
45
+ attr_values = {}
46
+ attr_names = {}
47
+
48
+ changes.each_with_index do |(field, (_old_val, new_val)), idx|
49
+ dynamo_key = record.class.to_dynamo_key(field)
50
+ if new_val.nil?
51
+ remove_parts << "#f#{idx}"
52
+ attr_names["#f#{idx}"] = dynamo_key
53
+ else
54
+ set_parts << "#f#{idx} = :v#{idx}"
55
+ attr_names["#f#{idx}"] = dynamo_key
56
+ attr_values[":v#{idx}"] = new_val
57
+ end
58
+ end
59
+
60
+ set_parts << "updatedAt = :ts"
61
+ attr_values[':ts'] = Time.now.utc.iso8601
62
+
63
+ update_expression = "SET #{set_parts.join(', ')}"
64
+ update_expression += " REMOVE #{remove_parts.join(', ')}" if remove_parts.any?
65
+
66
+ @operations << {
67
+ op: {
68
+ update: {
69
+ table_name: record.class.table_name,
70
+ key: { record.class.primary_key.to_s => record.id },
71
+ update_expression: update_expression,
72
+ expression_attribute_names: attr_names.any? ? attr_names : nil,
73
+ expression_attribute_values: attr_values
74
+ }.compact
75
+ },
76
+ record: record, type: :update
77
+ }
78
+ end
79
+
80
+ def execute!
81
+ return if @operations.empty?
82
+ raise TransactionError, "DynamoDB transactions are limited to #{MAX_ITEMS} items (got #{@operations.length})" if @operations.length > MAX_ITEMS
83
+
84
+ transact_items = @operations.map { |o| o[:op] }
85
+ client = @operations.first[:record].class.dynamodb
86
+ client.transact_write_items(transact_items: transact_items)
87
+
88
+ @operations.each do |o|
89
+ o[:record].instance_variable_set(:@new_record, false) if o[:type] == :put
90
+ end
91
+ rescue Aws::DynamoDB::Errors::TransactionCanceledException => e
92
+ raise TransactionError, "Transaction cancelled: #{e.message}"
93
+ rescue Aws::DynamoDB::Errors::ValidationException => e
94
+ raise TransactionError, "Transaction validation failed: #{e.message}"
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module ActiveItem
6
+ class UniquenessValidator < ActiveModel::EachValidator
7
+ def validate_each(record, attribute, value)
8
+ return if value.nil? || value.to_s.empty?
9
+
10
+ conditions = { attribute => value }
11
+ if options[:scope]
12
+ Array(options[:scope]).each do |scope_attr|
13
+ conditions[scope_attr] = record.send(scope_attr)
14
+ end
15
+ end
16
+
17
+ existing = record.class.where(**conditions)
18
+ existing = existing.reject { |r| r.id == record.id } if record.id
19
+ existing = existing.select { |r| options[:conditions].call(r) } if options[:conditions]
20
+
21
+ record.errors.add(attribute, options[:message] || 'has already been taken') if existing.any?
22
+ end
23
+ end
24
+
25
+ module Validations
26
+ def validates_uniqueness_of(*attributes, **options)
27
+ validates(*attributes, uniqueness: options.empty? ? true : options)
28
+ end
29
+
30
+ def validates_length_of(*attributes, **options)
31
+ attributes.each do |attribute|
32
+ validate do
33
+ value = send(attribute)
34
+ next if value.nil?
35
+
36
+ length = value.to_s.length
37
+
38
+ if options[:minimum] && length < options[:minimum]
39
+ errors.add(attribute, options[:message] || "is too short (minimum is #{options[:minimum]} characters)")
40
+ end
41
+ if options[:maximum] && length > options[:maximum]
42
+ errors.add(attribute, options[:message] || "is too long (maximum is #{options[:maximum]} characters)")
43
+ end
44
+ if options[:in] && !options[:in].include?(length)
45
+ errors.add(attribute, options[:message] || "length must be between #{options[:in].min} and #{options[:in].max} characters")
46
+ end
47
+ if options[:is] && length != options[:is]
48
+ errors.add(attribute, options[:message] || "must be exactly #{options[:is]} characters")
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def validates_numericality_of(*attributes, **options)
55
+ attributes.each do |attribute|
56
+ validate do
57
+ value = send(attribute)
58
+ next if value.nil?
59
+
60
+ unless value.is_a?(Numeric) || value.to_s.match?(/\A-?\d+(\.\d+)?\z/)
61
+ errors.add(attribute, options[:message] || "is not a number")
62
+ next
63
+ end
64
+
65
+ num_value = value.to_f
66
+
67
+ if options[:only_integer] && num_value != num_value.to_i
68
+ errors.add(attribute, options[:message] || "must be an integer")
69
+ next
70
+ end
71
+
72
+ if options[:greater_than] && num_value <= options[:greater_than]
73
+ errors.add(attribute, options[:message] || "must be greater than #{options[:greater_than]}")
74
+ end
75
+ if options[:greater_than_or_equal_to] && num_value < options[:greater_than_or_equal_to]
76
+ errors.add(attribute, options[:message] || "must be greater than or equal to #{options[:greater_than_or_equal_to]}")
77
+ end
78
+ if options[:less_than] && num_value >= options[:less_than]
79
+ errors.add(attribute, options[:message] || "must be less than #{options[:less_than]}")
80
+ end
81
+ if options[:less_than_or_equal_to] && num_value > options[:less_than_or_equal_to]
82
+ errors.add(attribute, options[:message] || "must be less than or equal to #{options[:less_than_or_equal_to]}")
83
+ end
84
+ if options[:equal_to] && num_value != options[:equal_to]
85
+ errors.add(attribute, options[:message] || "must be equal to #{options[:equal_to]}")
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def validates_format_of(*attributes, **options)
92
+ attributes.each { |attribute| validates attribute, format: options }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveItem
4
+ VERSION = '0.0.1'
5
+ end
data/lib/activeitem.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_item/version'
4
+ require_relative 'active_item/configuration'
5
+ require_relative 'active_item/logging'
6
+ require_relative 'active_item/errors'
7
+ require_relative 'active_item/database_helpers'
8
+ require_relative 'active_item/query_helpers'
9
+ require_relative 'active_item/relation'
10
+ require_relative 'active_item/associations'
11
+ require_relative 'active_item/composed_of'
12
+ require_relative 'active_item/validations'
13
+ require_relative 'active_item/transaction'
14
+ require_relative 'active_item/pagination'
15
+ require_relative 'active_item/base'
16
+
17
+ module ActiveItem
18
+ class << self
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield(configuration)
25
+ end
26
+
27
+ def logger
28
+ configuration.logger
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activeitem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andy Davis
8
+ - Adam Dalton
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-dynamodb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
83
+ description: A Rails-inspired ORM for DynamoDB with query builder, associations, callbacks,
84
+ dirty tracking, validations, transactions, and pagination.
85
+ email:
86
+ - andy@stowzilla.com
87
+ - adam@stowzilla.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - CHANGELOG.md
93
+ - LICENSE.txt
94
+ - README.md
95
+ - lib/active_item/associations.rb
96
+ - lib/active_item/base.rb
97
+ - lib/active_item/composed_of.rb
98
+ - lib/active_item/configuration.rb
99
+ - lib/active_item/database_helpers.rb
100
+ - lib/active_item/errors.rb
101
+ - lib/active_item/logging.rb
102
+ - lib/active_item/model_loader.rb
103
+ - lib/active_item/pagination.rb
104
+ - lib/active_item/query_helpers.rb
105
+ - lib/active_item/relation.rb
106
+ - lib/active_item/transaction.rb
107
+ - lib/active_item/validations.rb
108
+ - lib/active_item/version.rb
109
+ - lib/activeitem.rb
110
+ homepage: https://github.com/stowzilla/activeitem
111
+ licenses:
112
+ - MIT
113
+ metadata:
114
+ homepage_uri: https://github.com/stowzilla/activeitem
115
+ source_code_uri: https://github.com/stowzilla/activeitem
116
+ changelog_uri: https://github.com/stowzilla/activeitem/blob/main/CHANGELOG.md
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '3.1'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubygems_version: 4.0.11
132
+ specification_version: 4
133
+ summary: ActiveRecord-like ORM for AWS DynamoDB
134
+ test_files: []