dynomite 1.2.7 → 2.0.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/.gitignore +17 -2
- data/CHANGELOG.md +18 -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 +45 -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 +178 -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 +12 -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,90 @@
|
|
|
1
|
+
class Dynomite::Item
|
|
2
|
+
module Read
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
include Find
|
|
5
|
+
include FindWithEvent
|
|
6
|
+
include Query
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
# Override Enumerable#first to limit to 1 item as optimization
|
|
10
|
+
def first
|
|
11
|
+
all.limit(1).first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Adds very little wrapper logic to scan.
|
|
15
|
+
#
|
|
16
|
+
# * Automatically add table_name to options for convenience.
|
|
17
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
|
18
|
+
# dynamodb client response.
|
|
19
|
+
#
|
|
20
|
+
# Other than that, usage is same was using the dynamodb client scan method
|
|
21
|
+
# directly. Example:
|
|
22
|
+
#
|
|
23
|
+
# MyModel.scan(
|
|
24
|
+
# expression_attribute_names: {"#updated_at"=>"updated_at"},
|
|
25
|
+
# expression_attribute_values: {
|
|
26
|
+
# ":start_time" => "2010-01-01T00:00:00",
|
|
27
|
+
# ":end_time" => "2020-01-01T00:00:00"
|
|
28
|
+
# },
|
|
29
|
+
# filter_expression: "#updated_at between :start_time and :end_time",
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
|
33
|
+
def scan(params={})
|
|
34
|
+
params = { table_name: table_name }.merge(params)
|
|
35
|
+
resp = client.scan(params)
|
|
36
|
+
logger.info("REQUEST: #{params}")
|
|
37
|
+
resp.items.map {|i| self.new(i) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Adds very little wrapper logic to query.
|
|
41
|
+
#
|
|
42
|
+
# * Automatically add table_name to options for convenience.
|
|
43
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
|
44
|
+
# dynamodb client response.
|
|
45
|
+
#
|
|
46
|
+
# Other than that, usage is same was using the dynamodb client query method
|
|
47
|
+
# directly. Example:
|
|
48
|
+
#
|
|
49
|
+
# MyModel.query(
|
|
50
|
+
# index_name: 'category-index',
|
|
51
|
+
# expression_attribute_names: { "#category_name" => "category" },
|
|
52
|
+
# expression_attribute_values: { ":category_value" => "Entertainment" },
|
|
53
|
+
# key_condition_expression: "#category_name = :category_value",
|
|
54
|
+
# )
|
|
55
|
+
#
|
|
56
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
|
57
|
+
def query(params={})
|
|
58
|
+
params = { table_name: table_name }.merge(params)
|
|
59
|
+
resp = client.query(params)
|
|
60
|
+
resp.items.map { |i| self.new(i) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def count
|
|
64
|
+
if Dynomite.config.default_count_method.to_sym == :item_count
|
|
65
|
+
item_count
|
|
66
|
+
else
|
|
67
|
+
scan_count
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
alias_method :size, :count
|
|
71
|
+
|
|
72
|
+
def scan_count
|
|
73
|
+
warn_scan <<~EOL
|
|
74
|
+
WARN: Using scan to count. Though it is more accurate.
|
|
75
|
+
It can be slow and expensive. You can use item_count instead.
|
|
76
|
+
Note: item_count may be stale for about 6 hours.
|
|
77
|
+
You can set the Dynomite.config.default_count_method = :item_count to make it the default.
|
|
78
|
+
EOL
|
|
79
|
+
all.count
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Table.html#item_count-instance_method
|
|
83
|
+
# DynamoDB updates this value approximately every six hours.
|
|
84
|
+
def item_count
|
|
85
|
+
table = Aws::DynamoDB::Table.new(name: table_name, client: client)
|
|
86
|
+
table.item_count # fast but can be stale
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -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
|