dynomite 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -190
  7. data/Rakefile +13 -1
  8. data/dynomite.gemspec +9 -2
  9. data/exe/dynomite +14 -0
  10. data/lib/dynomite/associations/association.rb +126 -0
  11. data/lib/dynomite/associations/belongs_to.rb +35 -0
  12. data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
  13. data/lib/dynomite/associations/has_many.rb +19 -0
  14. data/lib/dynomite/associations/has_one.rb +19 -0
  15. data/lib/dynomite/associations/many_association.rb +257 -0
  16. data/lib/dynomite/associations/single_association.rb +157 -0
  17. data/lib/dynomite/associations.rb +248 -0
  18. data/lib/dynomite/autoloader.rb +25 -0
  19. data/lib/dynomite/cli.rb +48 -0
  20. data/lib/dynomite/client.rb +118 -0
  21. data/lib/dynomite/command.rb +89 -0
  22. data/lib/dynomite/completer/script.rb +6 -0
  23. data/lib/dynomite/completer/script.sh +10 -0
  24. data/lib/dynomite/completer.rb +159 -0
  25. data/lib/dynomite/config.rb +39 -0
  26. data/lib/dynomite/core.rb +18 -19
  27. data/lib/dynomite/engine.rb +45 -0
  28. data/lib/dynomite/erb.rb +5 -3
  29. data/lib/dynomite/error.rb +12 -0
  30. data/lib/dynomite/help/completion.md +20 -0
  31. data/lib/dynomite/help/completion_script.md +3 -0
  32. data/lib/dynomite/help/migrate.md +3 -0
  33. data/lib/dynomite/help.rb +9 -0
  34. data/lib/dynomite/install.rb +4 -0
  35. data/lib/dynomite/item/abstract.rb +15 -0
  36. data/lib/dynomite/item/components.rb +33 -0
  37. data/lib/dynomite/item/dsl.rb +101 -0
  38. data/lib/dynomite/item/id.rb +41 -0
  39. data/lib/dynomite/item/indexes/finder.rb +58 -0
  40. data/lib/dynomite/item/indexes/index.rb +21 -0
  41. data/lib/dynomite/item/indexes/primary_index.rb +18 -0
  42. data/lib/dynomite/item/indexes.rb +25 -0
  43. data/lib/dynomite/item/locking.rb +53 -0
  44. data/lib/dynomite/item/magic_fields.rb +66 -0
  45. data/lib/dynomite/item/primary_key.rb +85 -0
  46. data/lib/dynomite/item/query/delegates.rb +28 -0
  47. data/lib/dynomite/item/query/params/base.rb +42 -0
  48. data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
  49. data/lib/dynomite/item/query/params/filter.rb +41 -0
  50. data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
  51. data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
  52. data/lib/dynomite/item/query/params/function/base.rb +33 -0
  53. data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
  54. data/lib/dynomite/item/query/params/function/contains.rb +7 -0
  55. data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
  56. data/lib/dynomite/item/query/params/helpers.rb +94 -0
  57. data/lib/dynomite/item/query/params/key_condition.rb +34 -0
  58. data/lib/dynomite/item/query/params.rb +115 -0
  59. data/lib/dynomite/item/query/partiql/executer.rb +72 -0
  60. data/lib/dynomite/item/query/partiql.rb +67 -0
  61. data/lib/dynomite/item/query/relation/chain.rb +125 -0
  62. data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
  63. data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
  64. data/lib/dynomite/item/query/relation/delete.rb +38 -0
  65. data/lib/dynomite/item/query/relation/ids.rb +21 -0
  66. data/lib/dynomite/item/query/relation/math.rb +19 -0
  67. data/lib/dynomite/item/query/relation/where_field.rb +32 -0
  68. data/lib/dynomite/item/query/relation/where_group.rb +78 -0
  69. data/lib/dynomite/item/query/relation.rb +127 -0
  70. data/lib/dynomite/item/query.rb +7 -0
  71. data/lib/dynomite/item/read/find.rb +196 -0
  72. data/lib/dynomite/item/read/find_with_event.rb +42 -0
  73. data/lib/dynomite/item/read.rb +90 -0
  74. data/lib/dynomite/item/sti.rb +43 -0
  75. data/lib/dynomite/item/table_namespace.rb +43 -0
  76. data/lib/dynomite/item/typecaster.rb +106 -0
  77. data/lib/dynomite/item/waiter_methods.rb +18 -0
  78. data/lib/dynomite/item/write/base.rb +15 -0
  79. data/lib/dynomite/item/write/delete_item.rb +14 -0
  80. data/lib/dynomite/item/write/put_item.rb +99 -0
  81. data/lib/dynomite/item/write/update_item.rb +73 -0
  82. data/lib/dynomite/item/write.rb +204 -0
  83. data/lib/dynomite/item.rb +113 -286
  84. data/lib/dynomite/migration/dsl/accessor.rb +19 -0
  85. data/lib/dynomite/migration/dsl/index/base.rb +42 -0
  86. data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
  87. data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
  88. data/lib/dynomite/migration/dsl/index.rb +72 -0
  89. data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
  90. data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
  91. data/lib/dynomite/migration/dsl.rb +89 -142
  92. data/lib/dynomite/migration/file_info.rb +28 -0
  93. data/lib/dynomite/migration/generator.rb +30 -16
  94. data/lib/dynomite/migration/helpers.rb +7 -0
  95. data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
  96. data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
  97. data/lib/dynomite/migration/runner.rb +178 -0
  98. data/lib/dynomite/migration/templates/create_table.rb +7 -23
  99. data/lib/dynomite/migration/templates/delete_table.rb +7 -0
  100. data/lib/dynomite/migration/templates/update_table.rb +3 -18
  101. data/lib/dynomite/migration.rb +53 -10
  102. data/lib/dynomite/reserved_words.rb +13 -3
  103. data/lib/dynomite/seed.rb +12 -0
  104. data/lib/dynomite/types.rb +22 -0
  105. data/lib/dynomite/version.rb +1 -1
  106. data/lib/dynomite/waiter.rb +40 -0
  107. data/lib/dynomite.rb +11 -17
  108. data/lib/generators/application_item/application_item_generator.rb +30 -0
  109. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  110. data/lib/jets/commands/dynamodb_command.rb +29 -0
  111. data/lib/jets/commands/help/generate.md +33 -0
  112. data/lib/jets/commands/help/migrate.md +3 -0
  113. metadata +201 -17
  114. data/docs/migrations/long-example.rb +0 -127
  115. data/docs/migrations/short-example.rb +0 -40
  116. data/lib/dynomite/db_config.rb +0 -121
  117. data/lib/dynomite/errors.rb +0 -15
  118. data/lib/dynomite/log.rb +0 -15
  119. data/lib/dynomite/migration/common.rb +0 -86
  120. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  121. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  122. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  123. 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