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