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,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
|