dynomite 1.2.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,33 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Components
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
extend Indexes
|
7
|
+
extend Memoist
|
8
|
+
extend TableNamespace
|
9
|
+
|
10
|
+
include ActiveModel::Validations::Callbacks
|
11
|
+
|
12
|
+
define_model_callbacks :initialize, :find, :touch, only: :after
|
13
|
+
define_model_callbacks :save, :create, :update, :destroy
|
14
|
+
|
15
|
+
include PrimaryKey
|
16
|
+
include MagicFields # created_at, updated_at, partition_key (primary_key: id)
|
17
|
+
include Id
|
18
|
+
end
|
19
|
+
|
20
|
+
include Dynomite::Client
|
21
|
+
include Dsl
|
22
|
+
include ActiveModel::Model
|
23
|
+
include ActiveModel::Callbacks
|
24
|
+
include ActiveModel::Dirty
|
25
|
+
include ActiveModel::Serialization
|
26
|
+
include WaiterMethods
|
27
|
+
include Sti
|
28
|
+
include Locking
|
29
|
+
include Dynomite::Associations
|
30
|
+
include Read
|
31
|
+
include Write
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "dynomite/reserved_words"
|
2
|
+
|
3
|
+
class Dynomite::Item
|
4
|
+
module Dsl
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Defines column. Defined column can be accessed by getter and setter methods of the same
|
9
|
+
# name (e.g. [model.my_column]). Attributes with undefined columns can be accessed by
|
10
|
+
# [model.attrs] method.
|
11
|
+
def fields(*names)
|
12
|
+
if names.empty? # getter
|
13
|
+
fields_meta # meta info for all fields
|
14
|
+
else # setter
|
15
|
+
names.each(&method(:add_field))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
alias_method :columns, :fields
|
19
|
+
|
20
|
+
# @see Item.column
|
21
|
+
def add_field(name, options={})
|
22
|
+
name = name.to_sym
|
23
|
+
return if self.field_names.include?(name)
|
24
|
+
self.fields_map[name] = options # store original options for reference for fields_meta
|
25
|
+
|
26
|
+
if Dynomite::RESERVED_WORDS.include?(name.to_s)
|
27
|
+
raise Dynomite::Error::ReservedWord, "'#{name}' is a reserved word"
|
28
|
+
end
|
29
|
+
|
30
|
+
# https://guides.rubyonrails.org/active_model_basics.html#dirty
|
31
|
+
# Dirty support. IE: changed? and changed_attributes
|
32
|
+
# Requires us to define the attribute method this way
|
33
|
+
define_attribute_methods name # for dirty support
|
34
|
+
|
35
|
+
define_method(name) do
|
36
|
+
@attrs ||= {}
|
37
|
+
value = @attrs[name]
|
38
|
+
typecaster = Typecaster.new(self)
|
39
|
+
type = options[:type] || Dynomite.config.default_field_type
|
40
|
+
typecaster.cast_to_type(type, value)
|
41
|
+
end
|
42
|
+
|
43
|
+
define_method("#{name}=") do |value|
|
44
|
+
@attrs ||= {}
|
45
|
+
|
46
|
+
typecaster = Typecaster.new(self)
|
47
|
+
type = options[:type] || Dynomite.config.default_field_type
|
48
|
+
value_casted = typecaster.cast_to_type(type, value, on: :write)
|
49
|
+
old_value = read_attribute(name)
|
50
|
+
old_value = typecaster.cast_to_type(type, old_value, on: :write)
|
51
|
+
|
52
|
+
send "#{name}_will_change!" if old_value != value_casted # from define_attribute_methods *names
|
53
|
+
@attrs[name] = value_casted
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method("#{name}?") do
|
57
|
+
!!send(name)
|
58
|
+
end if options[:type] == :boolean
|
59
|
+
|
60
|
+
if default = options[:default]
|
61
|
+
method_name = "set_#{name}_default".to_sym
|
62
|
+
define_method(method_name) do
|
63
|
+
return unless read_attribute(name).nil?
|
64
|
+
value = case default
|
65
|
+
when Symbol
|
66
|
+
send(default)
|
67
|
+
when Proc
|
68
|
+
default.call
|
69
|
+
else
|
70
|
+
default
|
71
|
+
end
|
72
|
+
send("#{name}=", value)
|
73
|
+
end
|
74
|
+
before_save method_name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
alias_method :field, :add_field
|
78
|
+
alias_method :column, :add_field
|
79
|
+
|
80
|
+
def field_names
|
81
|
+
klass, field_names = self, []
|
82
|
+
while klass.respond_to?(:fields_map)
|
83
|
+
current_field_names = klass.fields_map.keys || []
|
84
|
+
field_names += current_field_names
|
85
|
+
klass = klass.superclass
|
86
|
+
end
|
87
|
+
field_names.sort
|
88
|
+
end
|
89
|
+
alias_method :column_names, :field_names
|
90
|
+
|
91
|
+
def fields_meta
|
92
|
+
klass, fields_meta = self, {}
|
93
|
+
while klass.respond_to?(:fields_map)
|
94
|
+
fields_meta.merge!(klass.fields_map)
|
95
|
+
klass = klass.superclass
|
96
|
+
end
|
97
|
+
fields_meta
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Id
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
field :id
|
7
|
+
before_save :set_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def set_id
|
11
|
+
return if self.class.disable_id?
|
12
|
+
self.id ||= generate_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_id
|
16
|
+
"#{id_prefix}-#{SecureRandom.alphanumeric(16)}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def id_prefix
|
20
|
+
self.class.id_prefix_value
|
21
|
+
end
|
22
|
+
|
23
|
+
class_methods do
|
24
|
+
def disable_id?
|
25
|
+
!!@disable_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def disable_id!
|
29
|
+
@disable_id = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def id_prefix(value=nil)
|
33
|
+
if value.nil?
|
34
|
+
self.id_prefix_value
|
35
|
+
else
|
36
|
+
self.id_prefix_value = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Dynomite::Item::Indexes
|
2
|
+
class Finder
|
3
|
+
extend Memoist
|
4
|
+
include Dynomite::Client
|
5
|
+
|
6
|
+
def initialize(source, query)
|
7
|
+
@source, @query = source, query
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(index_name=nil)
|
11
|
+
if index_name # explicit index name
|
12
|
+
index = @source.indexes.find { |i| i.index_name == index_name }
|
13
|
+
if index
|
14
|
+
return index
|
15
|
+
else
|
16
|
+
logger.info <<~EOL
|
17
|
+
WARN: Index #{index_name} specified but not found for table #{@source.table_name}
|
18
|
+
Falling back to auto-discovery of indexes
|
19
|
+
EOL
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# auto-discover
|
24
|
+
find_primary_key_index || find_secondary_index
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_primary_key_index
|
28
|
+
PrimaryIndex.new(@source.primary_key_fields) if primary_key_found?
|
29
|
+
end
|
30
|
+
|
31
|
+
def primary_key_found?
|
32
|
+
if @source.composite_key?
|
33
|
+
query_fields.include?(@source.partition_key_field) && query_fields.include?(@source.sort_key_field)
|
34
|
+
else
|
35
|
+
query_fields.include?(@source.partition_key_field)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# It's possible to have multiple indexes with the same partition and sort key.
|
40
|
+
# Will use the first one we find.
|
41
|
+
def find_secondary_index
|
42
|
+
@source.indexes.find do |i|
|
43
|
+
# If query field has comparision expression like
|
44
|
+
# Product.where("category.in": ["Electronics"]).count
|
45
|
+
# then it wont match, which is correct.
|
46
|
+
intersect = query_fields & i.fields
|
47
|
+
intersect == i.fields
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def query_fields
|
52
|
+
@query[:where].inject([]) do |result, where_group|
|
53
|
+
result += where_group.fields
|
54
|
+
end.uniq.sort.map(&:to_s)
|
55
|
+
end
|
56
|
+
memoize :query_fields
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dynomite::Item::Indexes
|
2
|
+
class Index
|
3
|
+
delegate :index_name, :key_schema, :projection, :index_status, :index_size_bytes, :item_count, :index_arn,
|
4
|
+
to: :data
|
5
|
+
|
6
|
+
attr_reader :data
|
7
|
+
def initialize(data)
|
8
|
+
@data = data # from describe_table.table.global_secondary_indexes items
|
9
|
+
end
|
10
|
+
|
11
|
+
def fields
|
12
|
+
key_schema.map do |hash|
|
13
|
+
hash.attribute_name
|
14
|
+
end.sort
|
15
|
+
end
|
16
|
+
|
17
|
+
def primary?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dynomite::Item::Indexes
|
2
|
+
class PrimaryIndex
|
3
|
+
attr_reader :fields
|
4
|
+
def initialize(fields)
|
5
|
+
@fields = fields
|
6
|
+
end
|
7
|
+
|
8
|
+
# primary index is the table itself
|
9
|
+
# no name. LSI and GSI have names.
|
10
|
+
def index_name
|
11
|
+
"primary_key (fields: #{fields.join(", ")})"
|
12
|
+
end
|
13
|
+
|
14
|
+
def primary?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Indexes
|
3
|
+
def index_names
|
4
|
+
indexes.map(&:index_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
# Sorted by indexes with composite keys with partition and sort keys first
|
8
|
+
# so they take priority for Indexes::Finder#find
|
9
|
+
def indexes
|
10
|
+
lsi = local_secondary_indexes.map { |i| Index.new(i) }.sort_by { |i| i.fields.size * -1 }
|
11
|
+
gsi = global_secondary_indexes.map { |i| Index.new(i) }.sort_by { |i| i.fields.size * -1 }
|
12
|
+
lsi + gsi
|
13
|
+
end
|
14
|
+
|
15
|
+
def local_secondary_indexes
|
16
|
+
table = desc_table(table_name)
|
17
|
+
table.local_secondary_indexes.to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
def global_secondary_indexes
|
21
|
+
table = desc_table(table_name)
|
22
|
+
table.global_secondary_indexes.to_a.select { |i| i.index_status == "ACTIVE" }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Locking
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :locking_field_name
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def enable_locking(field_name=:lock_version)
|
11
|
+
class_eval do
|
12
|
+
field field_name, type: :integer
|
13
|
+
self.locking_field_name = field_name
|
14
|
+
before_save :increment_lock_version
|
15
|
+
end
|
16
|
+
end
|
17
|
+
alias enable_optimistic_locking enable_locking
|
18
|
+
alias locking_field enable_locking
|
19
|
+
|
20
|
+
def locking_enabled?
|
21
|
+
locking_field_name.present?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment_lock_version
|
26
|
+
return unless changed?
|
27
|
+
reader = self.class.locking_field_name
|
28
|
+
setter = "#{reader}="
|
29
|
+
send(setter, 0) if send(reader).nil?
|
30
|
+
send(setter, send(reader) + 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Tricky: Must use dot notation for dirty tracking so old values are stored in case of a
|
34
|
+
# exceptional failure in the DynamoDB API put_item call in write/save.rb
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# post.update_attribute(:title, nil) # update attribute bypasses validations
|
38
|
+
#
|
39
|
+
# AWS Error:
|
40
|
+
#
|
41
|
+
# The AttributeValue for a key attribute cannot contain an empty string value.
|
42
|
+
# IndexName: title-index, IndexKey: title (Aws::DynamoDB::Errors::ValidationException)
|
43
|
+
#
|
44
|
+
# This allows the old value to be restored. And then the next update with a corrected
|
45
|
+
# title value saves successfully.
|
46
|
+
#
|
47
|
+
def reset_lock_version_was
|
48
|
+
reader = self.class.locking_field_name
|
49
|
+
setter = "#{reader}="
|
50
|
+
send(setter, send("#{reader}_was"))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module MagicFields
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
field :created_at, type: :time
|
7
|
+
field :updated_at, type: :time
|
8
|
+
before_save :set_created_at
|
9
|
+
before_save :set_updated_at
|
10
|
+
before_save :set_sort_key
|
11
|
+
before_save :set_partition_key
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_sort_key
|
15
|
+
return unless sort_key_field
|
16
|
+
return if @attrs[sort_key_field]
|
17
|
+
@attrs.merge!(sort_key_field => generate_random_key_schema_value("RANGE")) # RANGE is the sort key
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_partition_key
|
21
|
+
return if @attrs[partition_key_field]
|
22
|
+
if partition_key_field.to_s == "id"
|
23
|
+
@attrs.merge!(partition_key_field => generate_id) # IE: post-0GKjo3Ck0OBL6nAi
|
24
|
+
else
|
25
|
+
@attrs.merge!(partition_key_field => generate_random_key_schema_value("HASH")) # HASH is the partition key
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_random_key_schema_value(key_type)
|
30
|
+
attribute_name = key_schema.find { |a| a.key_type == key_type }.attribute_name
|
31
|
+
attribute_type = attribute_definitions.find { |a| a.attribute_name == attribute_name }.attribute_type
|
32
|
+
case attribute_type
|
33
|
+
when "N" # number
|
34
|
+
# 40 digit number that does not start with 0
|
35
|
+
first_digit = rand(1..9) # Generate a random digit from 1 to 9
|
36
|
+
rest_of_digits = Array.new(39) { rand(0..9) }.join # Generate the remaining 39 digits
|
37
|
+
random_number = "#{first_digit}#{rest_of_digits}"
|
38
|
+
random_number.to_i
|
39
|
+
when "S" # string
|
40
|
+
# 40 character string
|
41
|
+
Digest::SHA1.hexdigest([Time.now, rand].join) # IE: fead3c000892e9e8c78e821411bbaa9dc3cb938c
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_created_at
|
46
|
+
self.created_at ||= Time.now
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_updated_at
|
50
|
+
self.updated_at = Time.now if changed?
|
51
|
+
end
|
52
|
+
|
53
|
+
class_methods do
|
54
|
+
# Called in dynomite/engine.rb since need table name
|
55
|
+
def discover_fields!
|
56
|
+
return if abstract? # IE: ApplicationItem Dynomite::Item
|
57
|
+
attribute_definitions.each do |attr|
|
58
|
+
method_name = attr.attribute_name.to_sym
|
59
|
+
field method_name unless public_method_defined?(method_name)
|
60
|
+
end
|
61
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e
|
62
|
+
nil # Table does not exist yet
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module PrimaryKey
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
delegate :partition_key_field, :hash_key_field, :sort_key_field, :range_key_field,
|
6
|
+
:primary_key_fields, :primary_key, :composite_key, :composite_key?,
|
7
|
+
:attribute_definitions, :hash_key, :range_key, :key_schema,
|
8
|
+
to: :class
|
9
|
+
|
10
|
+
included do
|
11
|
+
before_update :check_primary_key_changed!
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_primary_key_changed!
|
15
|
+
if primary_key_changed?
|
16
|
+
changed_primary_keys = changed & primary_key_fields
|
17
|
+
raise Dynomite::Error::PrimaryKeyChangedError, "Cannot change the primary key of an existing record: #{changed_primary_keys}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def partition_key
|
22
|
+
send(partition_key_field) if partition_key_field
|
23
|
+
end
|
24
|
+
alias hash_key partition_key
|
25
|
+
|
26
|
+
def sort_key
|
27
|
+
send(sort_key_field) if sort_key_field
|
28
|
+
end
|
29
|
+
alias range_key sort_key
|
30
|
+
|
31
|
+
# Example: {category: "books", sku: "302"}
|
32
|
+
def primary_key
|
33
|
+
primary_key = {}
|
34
|
+
primary_key[partition_key_field.to_sym] = partition_key
|
35
|
+
primary_key[sort_key_field.to_sym] = sort_key if sort_key_field
|
36
|
+
primary_key
|
37
|
+
end
|
38
|
+
|
39
|
+
def primary_key_changed?
|
40
|
+
!(changed & primary_key_fields).empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
class_methods do
|
44
|
+
extend Memoist
|
45
|
+
|
46
|
+
def partition_key_field
|
47
|
+
discover_schema_key("HASH") unless abstract?
|
48
|
+
end
|
49
|
+
alias hash_key_field partition_key_field
|
50
|
+
|
51
|
+
def sort_key_field
|
52
|
+
discover_schema_key("RANGE") unless abstract?
|
53
|
+
end
|
54
|
+
alias range_key_field sort_key_field
|
55
|
+
|
56
|
+
def primary_key_fields
|
57
|
+
composite_key? ? composite_key : [partition_key_field] # ensure Array to make interface consistent
|
58
|
+
end
|
59
|
+
|
60
|
+
def composite_key
|
61
|
+
[partition_key_field, sort_key_field] if composite_key?
|
62
|
+
end
|
63
|
+
|
64
|
+
def composite_key?
|
65
|
+
!!sort_key_field
|
66
|
+
end
|
67
|
+
|
68
|
+
def attribute_definitions
|
69
|
+
table = desc_table(table_name)
|
70
|
+
table.attribute_definitions
|
71
|
+
end
|
72
|
+
|
73
|
+
def key_schema
|
74
|
+
table = desc_table(table_name)
|
75
|
+
table.key_schema
|
76
|
+
end
|
77
|
+
|
78
|
+
def discover_schema_key(key_type)
|
79
|
+
table = desc_table(table_name)
|
80
|
+
table.key_schema.find { |a| a.key_type == key_type }&.attribute_name
|
81
|
+
end
|
82
|
+
memoize :discover_schema_key
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dynomite::Item::Query
|
2
|
+
module Delegates
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Makes Relation methods like where, or, not, limit, etc available as model class methods.
|
6
|
+
# Post.where(category: 'ruby').limit(10)
|
7
|
+
class_methods do
|
8
|
+
delegates = Relation::Chain.public_instance_methods(false) +
|
9
|
+
Relation::Math.public_instance_methods(false) +
|
10
|
+
Relation::Ids.public_instance_methods(false) +
|
11
|
+
Relation::Delete.public_instance_methods(false)
|
12
|
+
delegates.each do |method|
|
13
|
+
delegate method, to: :all
|
14
|
+
end
|
15
|
+
|
16
|
+
# Most of thoese methods are free from Enumerable, except: last
|
17
|
+
delegate :last, :any?, :many?, :each_page, :pages, :raw_pages,
|
18
|
+
to: :all
|
19
|
+
|
20
|
+
# point of entry for query
|
21
|
+
def all
|
22
|
+
relation = Relation.new(self)
|
23
|
+
relation.where(type: name) if sti_enabled?
|
24
|
+
relation
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Dynomite::Item::Query::Params
|
2
|
+
class Base
|
3
|
+
extend Memoist
|
4
|
+
include Dynomite::Types
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
# To add function example:
|
8
|
+
# 1. params/base.rb function_names
|
9
|
+
# 2. query/chain.rb: add method
|
10
|
+
# 3. params/function/begins_with.rb: filter_expression, attribute_names, attribute_values
|
11
|
+
def function_names
|
12
|
+
%w[attribute_exists attribute_type begins_with contains size_fn]
|
13
|
+
end
|
14
|
+
|
15
|
+
def join_expressions
|
16
|
+
joined = ''
|
17
|
+
@expressions.each do |expression|
|
18
|
+
string = expression_string(expression)
|
19
|
+
if joined.empty? # first pass
|
20
|
+
joined << string
|
21
|
+
else
|
22
|
+
if !expression.is_a?(String) && expression.or?
|
23
|
+
joined << " OR #{string}"
|
24
|
+
else
|
25
|
+
joined << " AND #{string}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
joined
|
30
|
+
end
|
31
|
+
|
32
|
+
def expression_string(expression)
|
33
|
+
if expression.is_a?(String)
|
34
|
+
# Function filter expression is simple String
|
35
|
+
expression
|
36
|
+
else
|
37
|
+
# Else expression is CompressionExpression object with extra info like .or?
|
38
|
+
expression.build # build the string
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Dynomite::Item::Query::Params
|
2
|
+
class ExpressionAttribute < Base
|
3
|
+
def initialize(relation)
|
4
|
+
@relation = relation
|
5
|
+
@names, @values = {}, {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def names
|
9
|
+
build
|
10
|
+
@names
|
11
|
+
end
|
12
|
+
|
13
|
+
def values
|
14
|
+
build
|
15
|
+
@values
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
build_where
|
20
|
+
build_functions
|
21
|
+
build_project_expression
|
22
|
+
end
|
23
|
+
memoize :build
|
24
|
+
|
25
|
+
def build_where
|
26
|
+
with_where_groups do |where_group|
|
27
|
+
where_group.each do |where_field|
|
28
|
+
field = where_field.field
|
29
|
+
reference = where_field.reference
|
30
|
+
value = where_field.value
|
31
|
+
@names["##{reference}"] = field
|
32
|
+
|
33
|
+
model_class = @relation.source
|
34
|
+
meta = model_class.fields_meta[field.to_sym] # can be nil if field is not defined
|
35
|
+
type = meta ? meta[:type] : :infer
|
36
|
+
|
37
|
+
if where_field.operator == "in" || value.is_a?(Array)
|
38
|
+
array = Array(value)
|
39
|
+
array.each_with_index do |v, i|
|
40
|
+
@values[":#{reference}_#{i}"] = cast_to_type(type, v, on: :query)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
@values[":#{reference}"] = cast_to_type(type, value, on: :query)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
50
|
+
def build_functions
|
51
|
+
# Essentially
|
52
|
+
# @names.merge!(Function::AttributeExists.new(@query).attribute_names)
|
53
|
+
# @names.merge!(Function::AttributeType.new(@query).attribute_names)
|
54
|
+
# @names.merge!(Function::BeginsWith.new(@query).attribute_names)
|
55
|
+
function_names.each do |function_name|
|
56
|
+
function = Function.const_get(function_name.camelize).new(@relation.query)
|
57
|
+
@names.merge!(function.attribute_names)
|
58
|
+
@values.merge!(function.attribute_values)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_project_expression
|
63
|
+
return if @relation.query[:projection_expression].nil?
|
64
|
+
projection_expression = normalize_project_expression(@relation.query[:projection_expression])
|
65
|
+
projection_expression.each do |field|
|
66
|
+
field = field.to_s
|
67
|
+
@names["##{field}"] = field
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
delegate :cast_to_type, to: :typecaster
|
74
|
+
def typecaster
|
75
|
+
Dynomite::Item::Typecaster.new(@relation.source)
|
76
|
+
end
|
77
|
+
memoize :typecaster
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Dynomite::Item::Query::Params
|
2
|
+
class Filter < Base
|
3
|
+
def initialize(relation, index)
|
4
|
+
@relation, @index = relation, index
|
5
|
+
@expressions = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def build
|
9
|
+
build_where
|
10
|
+
build_functions
|
11
|
+
end
|
12
|
+
memoize :build
|
13
|
+
|
14
|
+
def expression
|
15
|
+
build
|
16
|
+
join_expressions
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_where
|
20
|
+
with_where_groups do |where_group|
|
21
|
+
expression = where_group.build_compare_expression_if do |field|
|
22
|
+
@index.nil? || !@index.fields.include?(field)
|
23
|
+
end
|
24
|
+
@expressions << expression if expression
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
29
|
+
def build_functions
|
30
|
+
# Essentially
|
31
|
+
# @expressions += Function::AttributeExists.new(@query).filter_expression
|
32
|
+
# @expressions += Function::AttributeType.new(@query).filter_expression
|
33
|
+
# @expressions += Function::BeginsWith.new(@query).filter_expression
|
34
|
+
function_names.each do |function_name|
|
35
|
+
function = Function.const_get(function_name.camelize).new(@relation.query)
|
36
|
+
filter_expression = function.filter_expression
|
37
|
+
@expressions += filter_expression
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|