dynomite 1.2.7 → 2.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 +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +21 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +15 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +179 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +13 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- data/lib/dynomite/migration/executor.rb +0 -38
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class Dynomite::Item
|
|
2
|
+
module Sti
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
class_attribute :inheritance_field_name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def enable_sti(field_name='type')
|
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
12
|
+
def self.inherited(subclass)
|
|
13
|
+
field :#{field_name} # IE: field_name :type
|
|
14
|
+
subclass.table_name(sti_base_table_name) # IE: subclass: Car base_table: vehicles
|
|
15
|
+
subclass.inheritance_field_name = :#{field_name}
|
|
16
|
+
|
|
17
|
+
before_save :set_type
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
22
|
+
alias inheritance_field enable_sti
|
|
23
|
+
|
|
24
|
+
def sti_base_table_name
|
|
25
|
+
klass = self
|
|
26
|
+
table_name = nil
|
|
27
|
+
until klass.abstract? # IE: ApplicationItem
|
|
28
|
+
table_name = klass.name.pluralize.gsub('::','_').underscore # vehicles
|
|
29
|
+
klass = klass.superclass
|
|
30
|
+
end
|
|
31
|
+
table_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sti_enabled?
|
|
35
|
+
inheritance_field_name.present?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_type
|
|
40
|
+
self[self.class.inheritance_field_name] = self.class.name
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class Dynomite::Item
|
|
2
|
+
module TableNamespace
|
|
3
|
+
def table_name(*args)
|
|
4
|
+
case args.size
|
|
5
|
+
when 0
|
|
6
|
+
get_table_name
|
|
7
|
+
when 1
|
|
8
|
+
set_table_name(args[0])
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set_table_name(value)
|
|
13
|
+
@table_name = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_table_name
|
|
17
|
+
@table_name ||= self.name.pluralize.gsub('::','_').underscore
|
|
18
|
+
[namespace, @table_name].reject {|s| s.nil? || s.empty?}.join(namespace_separator)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def namespace(*args)
|
|
22
|
+
case args.size
|
|
23
|
+
when 0
|
|
24
|
+
get_namespace
|
|
25
|
+
when 1
|
|
26
|
+
set_namespace(args[0])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_namespace
|
|
31
|
+
return @namespace if defined?(@namespace)
|
|
32
|
+
@namespace = Dynomite.config.namespace || Dynomite.config.default_namespace
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_namespace(value)
|
|
36
|
+
@namespace = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def namespace_separator
|
|
40
|
+
Dynomite.config.namespace_separator || '_'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
# aws-sdk-dynamodb handles typecast heavy-lifting. Adds typecasting support for DateTime objects.
|
|
4
|
+
class Dynomite::Item
|
|
5
|
+
class Typecaster
|
|
6
|
+
def initialize(model)
|
|
7
|
+
@model = model
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def dump(data, depth=0)
|
|
11
|
+
case data
|
|
12
|
+
when Array
|
|
13
|
+
data.map! { |v| dump(v, depth+1) }
|
|
14
|
+
when Hash
|
|
15
|
+
data.each_with_object({}) do |(k,v), dumped|
|
|
16
|
+
if depth == 0
|
|
17
|
+
v = cast_to_attribute_type(k, v) # cast to attribute type if defined
|
|
18
|
+
end
|
|
19
|
+
dumped[k] = dump(v, depth+1)
|
|
20
|
+
dumped
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
data # pass through
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# IE: field :price, type: :integer
|
|
28
|
+
# For most cases, we rely on aws-sdk-dynamodb to do the typecasting by inference.
|
|
29
|
+
#
|
|
30
|
+
# The method also helps keep track of where we cast_to_type
|
|
31
|
+
# It's only a few spots this provides an easy to search for it.
|
|
32
|
+
# See: https://rubyonjets.com/docs/database/dynamodb/model/typecasting/
|
|
33
|
+
FALSEY = [false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF']
|
|
34
|
+
def cast_to_type(type, value, on: :read)
|
|
35
|
+
case type
|
|
36
|
+
when :integer
|
|
37
|
+
value.to_i
|
|
38
|
+
when :boolean
|
|
39
|
+
!FALSEY.include?(value)
|
|
40
|
+
when :time
|
|
41
|
+
cast_to_time(value, on: on)
|
|
42
|
+
when :string
|
|
43
|
+
value.to_s # force to string
|
|
44
|
+
else # :infer
|
|
45
|
+
value # passthrough and let aws-sdk-dynamodb handle it
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# datetime to string
|
|
50
|
+
def cast_to_time(value, on: :read)
|
|
51
|
+
if on == :read
|
|
52
|
+
if value.is_a?(String)
|
|
53
|
+
Time.parse(value) # 2023-08-26T14:35:37Z
|
|
54
|
+
elsif value.respond_to?(:to_datetime) # time-like object already Time or DateTime
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
else # write or raw (for querying)
|
|
58
|
+
if value.respond_to?(:to_datetime) && !value.is_a?(String)
|
|
59
|
+
value.utc.strftime('%Y-%m-%dT%TZ') # Timestamp format iso8601 from AWS docs: http://amzn.to/2z98Bdc
|
|
60
|
+
else
|
|
61
|
+
value # passthrough string
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# string to float if attribute_type is N
|
|
67
|
+
# number to string if attribute_type is S
|
|
68
|
+
def cast_to_attribute_type(attribute_name, attribute_value)
|
|
69
|
+
definition = @model.attribute_definitions.find { |d| d[:attribute_name] == attribute_name.to_s }
|
|
70
|
+
if definition
|
|
71
|
+
case definition[:attribute_type]
|
|
72
|
+
when "N" # Number
|
|
73
|
+
attribute_value.to_f
|
|
74
|
+
when "S" # String
|
|
75
|
+
attribute_value.to_s
|
|
76
|
+
when "BOOL" # Boolean
|
|
77
|
+
attribute_value == true
|
|
78
|
+
else
|
|
79
|
+
attribute_value # passthrough
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
attribute_value # passthrough
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def load(data)
|
|
87
|
+
case data
|
|
88
|
+
when Array
|
|
89
|
+
data.map! { |v| load(v) }
|
|
90
|
+
when Hash
|
|
91
|
+
data.each_with_object({}) do |(k,v), loaded|
|
|
92
|
+
loaded[k] = load(v)
|
|
93
|
+
loaded
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
load_item(data)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
REGEXP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
|
|
101
|
+
def load_item(obj)
|
|
102
|
+
return obj unless obj.is_a?(String)
|
|
103
|
+
obj.match(REGEXP) ? Time.parse(obj) : obj
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Dynomite::Item::Write
|
|
2
|
+
class Base
|
|
3
|
+
include Dynomite::Client
|
|
4
|
+
|
|
5
|
+
# The attributes are in model.attrs and are held by reference
|
|
6
|
+
# The options are the client.delete_item or client.put_item options.
|
|
7
|
+
def self.call(model, options={})
|
|
8
|
+
new(model, options).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(model, options={})
|
|
12
|
+
@model, @options = model, options
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Dynomite::Item::Write
|
|
2
|
+
class DeleteItem < Base
|
|
3
|
+
def call
|
|
4
|
+
key = @model.attrs.slice(@model.class.partition_key_field, @model.class.sort_key_field)
|
|
5
|
+
params = {
|
|
6
|
+
table_name: @model.class.table_name,
|
|
7
|
+
key: key
|
|
8
|
+
}
|
|
9
|
+
# In case you want to specify condition_expression or expression_attribute_values
|
|
10
|
+
params = params.merge(@options)
|
|
11
|
+
client.delete_item(params) # resp
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Dynomite::Item::Write
|
|
2
|
+
class PutItem < Base
|
|
3
|
+
def call
|
|
4
|
+
# typecaster will convert the attrs to the correct types for saving to DynamoDB
|
|
5
|
+
item = Dynomite::Item::Typecaster.new(@model).dump(permitted_attrs)
|
|
6
|
+
@params = {
|
|
7
|
+
table_name: @model.class.table_name,
|
|
8
|
+
item: item
|
|
9
|
+
}
|
|
10
|
+
@params.merge!(check_unique_params)
|
|
11
|
+
@params.merge!(locking_params)
|
|
12
|
+
|
|
13
|
+
# put_item replaces the item fully. The resp does not contain the attrs.
|
|
14
|
+
log_debug(@params)
|
|
15
|
+
begin
|
|
16
|
+
client.put_item(@params)
|
|
17
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
|
18
|
+
handle_conditional_check_failed_exception(e)
|
|
19
|
+
rescue Aws::DynamoDB::Errors::ValidationException => e
|
|
20
|
+
@model.reset_lock_version_was if @model.class.locking_enabled?
|
|
21
|
+
raise
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@model.new_record = false
|
|
25
|
+
@model
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def permitted_attrs
|
|
29
|
+
field_names = @model.class.field_names.map(&:to_sym)
|
|
30
|
+
assigned_fields = @model.attrs.keys.map(&:to_sym)
|
|
31
|
+
undeclared_fields = assigned_fields - field_names
|
|
32
|
+
declared_fields = field_names - assigned_fields
|
|
33
|
+
|
|
34
|
+
case Dynomite.config.undeclared_field_behavior.to_sym
|
|
35
|
+
when :allow
|
|
36
|
+
@model.attrs # allow
|
|
37
|
+
when :silent
|
|
38
|
+
@model.attrs.slice(*field_names)
|
|
39
|
+
when :error
|
|
40
|
+
unless undeclared_fields.empty?
|
|
41
|
+
raise Dynomite::Error::UndeclaredFields.new("ERROR: Saving undeclared fields not allowed: #{undeclared_fields} for #{@model.class}")
|
|
42
|
+
end
|
|
43
|
+
else # warn
|
|
44
|
+
unless undeclared_fields.empty?
|
|
45
|
+
logger.info "WARNING: Not saving undeclared fields: #{undeclared_fields}. Saving declared fields only: #{declared_fields} for #{@model.class}"
|
|
46
|
+
end
|
|
47
|
+
@model.attrs.slice(*field_names)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def handle_conditional_check_failed_exception(exception)
|
|
52
|
+
if @params[:condition_expression] == check_unique_condition
|
|
53
|
+
raise Dynomite::Error::RecordNotUnique.new(not_unique_message)
|
|
54
|
+
else # currently only other case is locking
|
|
55
|
+
raise Dynomite::Error::StaleObject.new(exception.message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def not_unique_message
|
|
60
|
+
primary_key_attrs = permitted_attrs.stringify_keys.slice(@model.partition_key_field, @model.sort_key_field).symbolize_keys
|
|
61
|
+
primary_key_found = primary_key_attrs.keys.map(&:to_s)
|
|
62
|
+
"A #{@model.class.name} with the primary key #{primary_key_attrs} already exists"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check_unique_params
|
|
66
|
+
if @model.new_record? && !@options[:put]
|
|
67
|
+
@params.merge!(condition_expression: check_unique_condition)
|
|
68
|
+
else
|
|
69
|
+
{}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html#Expressions.ConditionExpressions.PreventingOverwrites
|
|
74
|
+
# Examples:
|
|
75
|
+
# attribute_not_exists(id)
|
|
76
|
+
# attribute_not_exists(category) AND attribute_not_exists(sku)
|
|
77
|
+
def check_unique_condition
|
|
78
|
+
condition_expression = @model.primary_key_fields.map do |field|
|
|
79
|
+
"attribute_not_exists(#{field})"
|
|
80
|
+
end.join(" AND ")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def locking_params
|
|
84
|
+
return {} if @params[:condition_expression] # already set from check_unique_params
|
|
85
|
+
return {} unless @model.class.locking_enabled?
|
|
86
|
+
return {} if @model._touching
|
|
87
|
+
field = @model.class.locking_field_name
|
|
88
|
+
current_version = @model.send(field) # must use send, since it was set by send. fixes .touch method
|
|
89
|
+
return {} if current_version == 1
|
|
90
|
+
|
|
91
|
+
previous_version = current_version - 1 # since before_save increments it
|
|
92
|
+
{
|
|
93
|
+
condition_expression: "#lock_version = :lock_version",
|
|
94
|
+
expression_attribute_names: {"#lock_version" => "lock_version"},
|
|
95
|
+
expression_attribute_values: {":lock_version" => previous_version}
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Dynomite::Item::Write
|
|
2
|
+
class UpdateItem < Base
|
|
3
|
+
def initialize(model, options={})
|
|
4
|
+
super
|
|
5
|
+
@attrs = {}
|
|
6
|
+
@count_changes = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Note: fields assigned directly with brackets are not tracked as changed
|
|
10
|
+
# IE: post[:title] = "test"
|
|
11
|
+
def call
|
|
12
|
+
changed_fields = @model.changed_attributes.keys
|
|
13
|
+
return if changed_fields.empty? # no changes to save
|
|
14
|
+
@attrs = @model.attrs.slice(*changed_fields)
|
|
15
|
+
log_debug(params)
|
|
16
|
+
client.update_item(params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Allows updates to specific attributes and counters
|
|
20
|
+
def save_changes(changes={})
|
|
21
|
+
@attrs = changes[:attrs] || {}
|
|
22
|
+
@count_changes = changes[:count_changes] || {}
|
|
23
|
+
log_debug(params)
|
|
24
|
+
client.update_item(params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def params
|
|
28
|
+
{
|
|
29
|
+
expression_attribute_names: expression_attribute_names, # { "##{attribute}" => attribute },
|
|
30
|
+
expression_attribute_values: expression_attribute_values, # { ':attribute' => value } or { ':by' => by }
|
|
31
|
+
update_expression: update_expression, # "SET ##{attribute} = ##{attribute}" or "SET ##{attribute} = ##{attribute} + :by"
|
|
32
|
+
key: @model.primary_key,
|
|
33
|
+
table_name: @model.class.table_name
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
alias to_params params
|
|
37
|
+
|
|
38
|
+
def expression_attribute_names
|
|
39
|
+
attr_names = @attrs.inject({}) do |names, (name,_)|
|
|
40
|
+
names.merge!("##{name}" => name)
|
|
41
|
+
end
|
|
42
|
+
count_names = @count_changes.inject({}) do |names, (name,_)|
|
|
43
|
+
names.merge!("##{name}" => name)
|
|
44
|
+
end
|
|
45
|
+
attr_names.merge(count_names)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def expression_attribute_values
|
|
49
|
+
typecaster = Dynomite::Item::Typecaster.new(@model)
|
|
50
|
+
attr_values = @attrs.inject({}) do |values, (name,value)|
|
|
51
|
+
meta = @model.class.fields_meta[name.to_sym] # can be nil if field is not defined
|
|
52
|
+
type = meta ? meta[:type] : :infer
|
|
53
|
+
value = typecaster.cast_to_type(type, value, on: :write)
|
|
54
|
+
values.merge!(":#{name}" => value)
|
|
55
|
+
end
|
|
56
|
+
count_values = @count_changes.inject({}) do |values, (name,value)|
|
|
57
|
+
values.merge!(":#{name}" => value)
|
|
58
|
+
end
|
|
59
|
+
attr_values.merge(count_values)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update_expression
|
|
63
|
+
expressions = []
|
|
64
|
+
@attrs.inject([]) do |exp, (name,_)|
|
|
65
|
+
expressions << "##{name} = :#{name}"
|
|
66
|
+
end
|
|
67
|
+
@count_changes.inject([]) do |exp, (name,_)|
|
|
68
|
+
expressions << "##{name} = ##{name} + :#{name}"
|
|
69
|
+
end
|
|
70
|
+
"SET " + expressions.join(', ')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
class Dynomite::Item
|
|
2
|
+
module Write
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# Not using method_missing to allow usage of dot notation and assign
|
|
6
|
+
# @attrs because it might hide actual missing methods errors.
|
|
7
|
+
# DynamoDB attrs can go many levels deep so it makes less make sense to
|
|
8
|
+
# use to dot notation.
|
|
9
|
+
|
|
10
|
+
def save(options={})
|
|
11
|
+
options.reverse_merge!(validate: true)
|
|
12
|
+
return self if options[:validate] && !valid?
|
|
13
|
+
|
|
14
|
+
action = new_record? ? :create : :update
|
|
15
|
+
run_callbacks(:save) do
|
|
16
|
+
run_callbacks(action) do
|
|
17
|
+
if action == :create
|
|
18
|
+
PutItem.call(self, options)
|
|
19
|
+
else # :update
|
|
20
|
+
call_update_strategy(options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Similar to save, but raises an error on failed validation.
|
|
27
|
+
def save!(options={})
|
|
28
|
+
raise_error_if_invalid
|
|
29
|
+
save(options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# post.update(title: "test", body: "body")
|
|
33
|
+
# post.update({title: "test", body: "body"}, {validate: false})
|
|
34
|
+
def update(attrs={}, options={})
|
|
35
|
+
self.attrs.merge!(attrs)
|
|
36
|
+
options.reverse_merge!(validate: true)
|
|
37
|
+
return false if options[:validate] && !valid?
|
|
38
|
+
|
|
39
|
+
run_callbacks(:save) do
|
|
40
|
+
run_callbacks(:update) do
|
|
41
|
+
call_update_strategy(options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call_update_strategy(options)
|
|
47
|
+
if Dynomite.config.update_strategy == :update_item
|
|
48
|
+
# Note: fields assigned directly with brackets are not tracked as changed
|
|
49
|
+
# IE: post[:title] = "test"
|
|
50
|
+
UpdateItem.call(self, options)
|
|
51
|
+
else # default
|
|
52
|
+
PutItem.call(self, options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Similar to update, but raises an error on failed validation.
|
|
57
|
+
def update!(attrs={}, options={})
|
|
58
|
+
raise_error_if_invalid
|
|
59
|
+
update(attrs, options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# When you add an item, the primary key attributes are the only required attributes.
|
|
63
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
|
64
|
+
def put(options={})
|
|
65
|
+
found_primary_keys = self.attrs.keys.map(&:to_s) & primary_key_fields
|
|
66
|
+
unless primary_key_fields.sort == found_primary_keys
|
|
67
|
+
raise Dynomite::Error::InvalidPut.new("Invalid put. The primary key fields #{primary_key_fields} must be present in the attrs #{attrs}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
options.reverse_merge!(validate: true)
|
|
71
|
+
return self if options[:validate] && !valid? # return self so can grab errors in invalid. save does the same thing
|
|
72
|
+
|
|
73
|
+
# Run callbacks for put so id is also set
|
|
74
|
+
run_callbacks(:save) do
|
|
75
|
+
run_callbacks(:update) do
|
|
76
|
+
PutItem.call(self, options.merge(put: true))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
alias replace put
|
|
81
|
+
|
|
82
|
+
def put!(options={})
|
|
83
|
+
raise_error_if_invalid
|
|
84
|
+
put(options)
|
|
85
|
+
end
|
|
86
|
+
alias replace! put!
|
|
87
|
+
|
|
88
|
+
def destroy(options={})
|
|
89
|
+
run_callbacks(:destroy) do
|
|
90
|
+
DeleteItem.call(self, options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete(options={})
|
|
95
|
+
DeleteItem.call(self, options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
attr_reader :_touching
|
|
99
|
+
def touch(*names, **options)
|
|
100
|
+
if new_record?
|
|
101
|
+
raise Dynomite::Error, 'cannot touch on a new item'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
time_to_assign = options.delete(:time) || Time.now
|
|
105
|
+
|
|
106
|
+
self.updated_at = time_to_assign
|
|
107
|
+
names.each do |name|
|
|
108
|
+
attrs.send("#{name}=", time_to_assign)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@_touching = true
|
|
112
|
+
run_callbacks :touch do
|
|
113
|
+
UpdateItem.call(self, options)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Examples:
|
|
120
|
+
# user.increment(:likes)
|
|
121
|
+
# user.increment(:likes, 2)
|
|
122
|
+
def increment(attribute, by = 1)
|
|
123
|
+
self[attribute] ||= 0
|
|
124
|
+
self[attribute] += by
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Increment counter. Validations and callbacks are skipped.
|
|
129
|
+
#
|
|
130
|
+
# Examples:
|
|
131
|
+
#
|
|
132
|
+
# user.increment!(:likes)
|
|
133
|
+
# user.increment!(:likes, 2)
|
|
134
|
+
# user.increment!(:likes, touch: true)
|
|
135
|
+
# user.increment!(:likes, touch: :created_at)
|
|
136
|
+
# user.increment!(:likes, touch: [:viewed_at, :created_at])
|
|
137
|
+
#
|
|
138
|
+
def increment!(attribute, by = 1, touch: nil)
|
|
139
|
+
increment(attribute, by)
|
|
140
|
+
|
|
141
|
+
now = Time.now
|
|
142
|
+
attrs = Array(touch).inject({}) do |attrs, field|
|
|
143
|
+
attrs.merge!(field => now)
|
|
144
|
+
end if touch
|
|
145
|
+
|
|
146
|
+
run_callbacks :touch do
|
|
147
|
+
UpdateItem.new(self).save_changes(attrs: attrs, count_changes: { attribute => by })
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Example:
|
|
154
|
+
#
|
|
155
|
+
# user = User.first
|
|
156
|
+
# user.banned? # => false
|
|
157
|
+
# user.toggle(:banned)
|
|
158
|
+
# user.banned? # => true
|
|
159
|
+
#
|
|
160
|
+
def toggle(attribute)
|
|
161
|
+
self[attribute] = !public_send("#{attribute}?")
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def toggle!(attribute)
|
|
166
|
+
toggle(attribute).update_attribute(attribute, self[attribute])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def raise_error_if_invalid
|
|
170
|
+
raise Dynomite::Error::Validation, "Validation failed: #{errors.full_messages.join(', ')}" unless valid?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class_methods do
|
|
174
|
+
def put(attrs={}, &block)
|
|
175
|
+
new(attrs, &block).put
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def put!(attrs={}, &block)
|
|
179
|
+
new(attrs, &block).put!
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def create(attrs={}, &block)
|
|
183
|
+
new(attrs, &block).save
|
|
184
|
+
end
|
|
185
|
+
alias create_with create
|
|
186
|
+
|
|
187
|
+
def create!(attrs={}, &block)
|
|
188
|
+
new(attrs, &block).save!
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def find_or_create_by(attrs={})
|
|
192
|
+
find_by(attrs) || create(attrs)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def find_or_create_by!(attrs={})
|
|
196
|
+
find_by(attrs) || create!(attrs)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def find_or_initialize_by(attrs)
|
|
200
|
+
find_by(attrs) || new(attrs)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|