eluvia-base 3.37.1

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +225 -0
  4. data/lib/eluvia/active_model/amount_attribute.rb +108 -0
  5. data/lib/eluvia/active_model/array_attribute.rb +134 -0
  6. data/lib/eluvia/active_model/attributes.rb +417 -0
  7. data/lib/eluvia/active_model/enum_array_attribute.rb +60 -0
  8. data/lib/eluvia/active_model/enum_attribute.rb +174 -0
  9. data/lib/eluvia/active_model/object_attribute.rb +143 -0
  10. data/lib/eluvia/active_model/range_attribute.rb +74 -0
  11. data/lib/eluvia/active_record/file_array_attribute.rb +116 -0
  12. data/lib/eluvia/active_record/file_attribute.rb +158 -0
  13. data/lib/eluvia/active_record/filtering.rb +680 -0
  14. data/lib/eluvia/active_record/filters_model.rb +58 -0
  15. data/lib/eluvia/active_record/ordering.rb +110 -0
  16. data/lib/eluvia/base/config.rb +38 -0
  17. data/lib/eluvia/base/version.rb +12 -0
  18. data/lib/eluvia/errors/bad_request.rb +17 -0
  19. data/lib/eluvia/errors/forbidden.rb +17 -0
  20. data/lib/eluvia/errors/not_found.rb +17 -0
  21. data/lib/eluvia/errors/service_unavailable.rb +17 -0
  22. data/lib/eluvia/errors/standard_error.rb +33 -0
  23. data/lib/eluvia/errors/unauthorized.rb +17 -0
  24. data/lib/eluvia/errors/unprocessable_entity.rb +26 -0
  25. data/lib/eluvia/fieldset/rest_field.rb +83 -0
  26. data/lib/eluvia/fieldset/rest_fieldset.rb +158 -0
  27. data/lib/eluvia/fieldset.rb +12 -0
  28. data/lib/eluvia/handlers/error_handler.rb +60 -0
  29. data/lib/eluvia/handlers/pagination_handler.rb +25 -0
  30. data/lib/eluvia/handlers/params_handler.rb +43 -0
  31. data/lib/eluvia/helpers/attachment_helper.rb +37 -0
  32. data/lib/eluvia/integrations/eluvia_integration.rb +281 -0
  33. data/lib/eluvia/models/file.rb +44 -0
  34. data/lib/eluvia/models/file_wrapper.rb +38 -0
  35. data/lib/eluvia/models/image_wrapper.rb +38 -0
  36. data/lib/eluvia/serializers/error_serializer.rb +21 -0
  37. data/lib/eluvia/serializers/fieldset_serializer.rb +63 -0
  38. data/lib/eluvia/services/health_service.rb +56 -0
  39. data/lib/eluvia/uploads.rb +13 -0
  40. data/lib/eluvia/utils/hash_and_array.rb +98 -0
  41. data/lib/eluvia/utils/jwt.rb +36 -0
  42. data/lib/eluvia/utils/string.rb +31 -0
  43. data/lib/eluvia/utils/uuid.rb +21 -0
  44. data/lib/eluvia-base.rb +79 -0
  45. metadata +186 -0
@@ -0,0 +1,143 @@
1
+ module Eluvia
2
+ module ActiveModel
3
+ module ObjectAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::Attributes
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Clean and transform value methods
11
+ # *********************************************************************
12
+
13
+ def clean_object_value(value, attr_name)
14
+ return nil if value.nil?
15
+ attr_spec = self.find_object_attr(attr_name)
16
+ raise Eluvia::Errors::StandardError.new("Cannot clean value of unknow attribute `#{attr_name}`.") unless attr_spec
17
+ raise Eluvia::Errors::StandardError.new("Invalid object value format: #{value.inspect}") unless value.is_a?(::Hash)
18
+
19
+ # Convert all keys to symbols for easier access
20
+ value = value.symbolize_keys
21
+
22
+ # Iterate fields and extract values
23
+ cleaned_value = {}
24
+ attr_spec[:fields].each do |field|
25
+ key = field.keys.first.to_sym
26
+ cleaned_value[key] = value.key?(key) ? value[key] : nil
27
+ end
28
+ cleaned_value
29
+ end
30
+
31
+ # *********************************************************************
32
+ # Attribute store methods
33
+ # *********************************************************************
34
+
35
+ def _object_attrs
36
+ @object_attrs ||= {}
37
+ end
38
+
39
+ def object_attrs
40
+ _object_attrs.values
41
+ end
42
+
43
+ def object_attr_names
44
+ object_attrs.map { |a| a[:attr_name] }
45
+ end
46
+
47
+ def has_object_attr?(attr_name)
48
+ _object_attrs.key?(attr_name.to_sym)
49
+ end
50
+
51
+ def find_object_attr(attr_name)
52
+ _object_attrs.fetch(attr_name.to_sym, nil)
53
+ end
54
+
55
+ # *********************************************************************
56
+ # Attribute definition methods for object
57
+ # *********************************************************************
58
+
59
+ # Add a new "object" attribute to this model
60
+ #
61
+ # @param attr_name [Symbol] Attribute name
62
+ # @param fields [Array<Symbol,String,Hash>] Underlying attribute names or mapping
63
+ # @param declare [Boolean] Whether to declare the underlying attribute
64
+ def object_attr(attr_name, fields, declare: false)
65
+ attr_name = attr_name.to_sym
66
+
67
+ # Declare underlying attribute
68
+ if declare
69
+ any_attr attr_name
70
+ end
71
+
72
+ # Check fields input
73
+ raise Eluvia::Errors::StandardError.new('Please specify object fields as an array.') unless fields.is_a?(::Array)
74
+ fields = fields.map do |field|
75
+ if field.is_a?(::Hash)
76
+ key = field.keys.first.to_sym
77
+ underlying_attr_name = field.values.first.to_sym
78
+ elsif field.is_a?(::Symbol) || field.is_a?(::String)
79
+ key = underlying_attr_name = field.to_sym
80
+ end
81
+ { key => underlying_attr_name }
82
+ end
83
+
84
+ # Register metadata
85
+ self._object_attrs[attr_name] = {
86
+ attr_name: attr_name,
87
+ fields: fields
88
+ }
89
+
90
+ # *******************************************************************
91
+ # Dynamically defined instance methods for object
92
+ # *******************************************************************
93
+
94
+ # Set method
95
+ define_method(:"#{attr_name}=") do |value|
96
+
97
+ # Convert string to object, we expect JSON string here
98
+ if value.is_a?(::String)
99
+ if !value.blank?
100
+ value = JSON.parse(value) rescue nil
101
+ else
102
+ value = nil
103
+ end
104
+ end
105
+
106
+ # Check input
107
+ raise Eluvia::Errors::BadRequest.new("Invalid value for object attribute `#{attr_name}`, expecting Hash or nil.") unless value.nil? || value.is_a?(::Hash)
108
+
109
+ # Handle blank value
110
+ if value.blank?
111
+ return
112
+ end
113
+
114
+ # Set values to underlying attributes if defined as fields
115
+ symbolized_value = value.symbolize_keys
116
+ fields.each do |field|
117
+ key = field.keys.first.to_sym
118
+ underlying_attr_name = field.values.first.to_sym
119
+ if symbolized_value.key?(key)
120
+ self.send(:"#{underlying_attr_name}=", symbolized_value[key])
121
+ end
122
+ end
123
+ end
124
+
125
+ # Get method
126
+ define_method(attr_name) do
127
+
128
+ # Set values of underlying attributes if defined as fields
129
+ cleaned_value = {}
130
+ fields.each do |field|
131
+ key = field.keys.first.to_sym
132
+ underlying_attr_name = field.values.first.to_sym
133
+ cleaned_value[key] = self.send(underlying_attr_name)
134
+ end
135
+
136
+ cleaned_value
137
+ end
138
+ end
139
+
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,74 @@
1
+ module Eluvia
2
+ module ActiveModel
3
+ module RangeAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::ObjectAttribute
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Clean and transform value methods
11
+ # *********************************************************************
12
+
13
+ def clean_range_value(value, attr_name)
14
+ clean_object_value(value, attr_name)
15
+ end
16
+
17
+ # *********************************************************************
18
+ # Attribute store methods
19
+ # *********************************************************************
20
+
21
+ def _range_attrs
22
+ @range_attrs ||= {}
23
+ end
24
+
25
+ def range_attrs
26
+ _range_attrs.values
27
+ end
28
+
29
+ def range_attr_names
30
+ range_attrs.map { |a| a[:attr_name] }
31
+ end
32
+
33
+ def has_range_attr?(attr_name)
34
+ _range_attrs.key?(attr_name.to_sym)
35
+ end
36
+
37
+ def find_range_attr(attr_name)
38
+ _range_attrs.fetch(attr_name.to_sym, nil)
39
+ end
40
+
41
+ # *********************************************************************
42
+ # Attribute definition methods for range
43
+ # *********************************************************************
44
+
45
+ # Add a new "range" attribute to this model
46
+ #
47
+ # @param attr_name [Symbol] Attribute name
48
+ # @param lower_attr_name [Symbol] Lower bound attribute name
49
+ # @param upper_attr_name [Symbol] Upper bound attribute name
50
+ # @param declare [Boolean] Whether to declare the underlying attribute as any
51
+ def range_attr(attr_name, lower_attr_name, upper_attr_name, declare: false)
52
+ attr_name = attr_name.to_sym
53
+ lower_attr_name = lower_attr_name.to_sym
54
+ upper_attr_name = upper_attr_name.to_sym
55
+
56
+ # Range is an object with lower and upper attributes
57
+ object_attr(attr_name,
58
+ [
59
+ { lower: lower_attr_name },
60
+ { upper: upper_attr_name }
61
+ ], declare: declare)
62
+
63
+ # Register metadata
64
+ self._range_attrs[attr_name] = {
65
+ attr_name: attr_name,
66
+ lower_attr_name: lower_attr_name,
67
+ upper_attr_name: upper_attr_name
68
+ }
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,116 @@
1
+ module Eluvia
2
+ module ActiveRecord
3
+ module FileArrayAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::ArrayAttribute
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Attribute definition methods for file array
11
+ # *********************************************************************
12
+
13
+ # Add a new "file array" attribute to this model
14
+ #
15
+ # @param attr_name [Symbol] Attribute name
16
+ # @param underlying_attachments [Symbol, nil] Name of the attribute holding underlying Active Storage attachments
17
+ # @param underlying_filter [Proc, nil] Optional filter to be applied to scope underlying attachments
18
+ # @param nature [Symbol] Nature of file attribute (:file, :image, etc.)
19
+ # @param declare [Boolean] Whether to declare the underlying attribute
20
+ def file_array_attr(attr_name, underlying_attachments: nil, underlying_filter: nil, nature: :file, declare: false)
21
+ attr_name = attr_name.to_sym
22
+ nature = nature.to_sym
23
+
24
+ # Define it using "has many" attribute
25
+ if declare
26
+ has_many_attr(attr_name, nature == :image ? 'Eluvia::ImageWrapper' : 'Eluvia::FileWrapper')
27
+ end
28
+
29
+ # *******************************************************************
30
+ # Dynamically defined instance methods for file array
31
+ # *******************************************************************
32
+
33
+ if underlying_attachments
34
+
35
+ # Set method
36
+ define_method(:"#{attr_name}=") do |new_values|
37
+
38
+ # Check input
39
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}`, expecting Array.") unless new_values.is_a?(::Array)
40
+
41
+ # Current values
42
+ current_values = self.send(attr_name)
43
+ current_ids = current_values.map(&:id)
44
+ new_ids = []
45
+
46
+ # Create missing attachments
47
+ new_values.each do |value|
48
+ # Normalize and check value
49
+ if value.is_a?(::Hash)
50
+ if nature == :image
51
+ value = Eluvia::ImageWrapper.map(value)
52
+ else
53
+ value = Eluvia::FileWrapper.map(value)
54
+ end
55
+ end
56
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[]`, expecting Eluvia::FileWrapper or Eluvia::ImageWrapper.") unless value.is_a?(Eluvia::FileWrapper) || value.is_a?(Eluvia::ImageWrapper)
57
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[].file`, can't be blank.") if value.file.blank?
58
+
59
+ # Keep track of new ids
60
+ new_ids << value.id if value.id
61
+
62
+ # Retrieve filename and content
63
+ filename = value.file&.filename
64
+ content = value.file&.content
65
+
66
+ # Sanitize filename
67
+ filename = ActiveStorage::Filename.new(filename).sanitized
68
+
69
+ # Create or update attachment only if both filename and content are passed (this means that user uploaded a new file)
70
+ if !filename.blank? && !content.blank?
71
+
72
+ # Delete the existing attachment
73
+ if value.id && current_ids.include?(value.id)
74
+ attachment_record = self.send("#{underlying_attachments}_attachments").find(value.id)
75
+ attachment_record.purge_later
76
+ end
77
+
78
+ # Create a new attachment
79
+ self.send(underlying_attachments).attach(io: StringIO.new(Base64.decode64(content)), filename: filename)
80
+ end
81
+
82
+ end
83
+
84
+ # Delete no longer wanted attachments
85
+ current_values.each do |value|
86
+ unless new_ids.include?(value.id)
87
+ attachment_record = self.send("#{underlying_attachments}_attachments").find(value.id)
88
+ attachment_record.purge_later
89
+ end
90
+ end
91
+ end
92
+
93
+ # Get method
94
+ define_method(attr_name) do
95
+ # must be sorted in memory and not in SQL in order to support prefetch via "includes"
96
+ attachments_records = self.send("#{underlying_attachments}_attachments").to_a.sort_by(&:created_at)
97
+ if underlying_filter && underlying_filter.respond_to?(:call)
98
+ attachments_records = attachments_records.select { |attachment_record| underlying_filter.call(attachment_record) }
99
+ end
100
+ attachments_records.map do |attachment|
101
+ if nature == :image
102
+ Eluvia::ImageWrapper.new(id: attachment.id, file: Eluvia::File.from_active_storage_attachment(attachment, skip_check: true))
103
+ else
104
+ Eluvia::FileWrapper.new(id: attachment.id, file: Eluvia::File.from_active_storage_attachment(attachment, skip_check: true))
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,158 @@
1
+ module Eluvia
2
+ module ActiveRecord
3
+ module FileAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::Attributes
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Attribute store methods
11
+ # *********************************************************************
12
+
13
+ def _file_attrs
14
+ @file_attrs ||= {}
15
+ end
16
+
17
+ def file_attrs
18
+ _file_attrs.values
19
+ end
20
+
21
+ def file_attr_names
22
+ file_attrs.map { |a| a[:attr_name] }
23
+ end
24
+
25
+ def has_file_attr?(attr_name)
26
+ _file_attrs.key?(attr_name.to_sym)
27
+ end
28
+
29
+ def find_file_attr(attr_name)
30
+ _file_attrs.fetch(attr_name.to_sym, nil)
31
+ end
32
+
33
+ # *********************************************************************
34
+ # Attribute definition methods for file
35
+ # *********************************************************************
36
+
37
+ # Add a new "file" attribute to this model
38
+ #
39
+ # @param attr_name [Symbol] Attribute name
40
+ # @param underlying_attachment [Symbol, nil] Name of the attribute holding underlying Active Storage attachment
41
+ # @param nature [Symbol] Nature of file attribute (:file, :image, etc.)
42
+ # @param declare [Boolean] Whether to declare the underlying attribute
43
+ def file_attr(attr_name, underlying_attachment: nil, nature: :file, declare: false)
44
+ attr_name = attr_name.to_sym
45
+ nature = nature.to_sym
46
+
47
+ # Declare underlying "has one" attribute
48
+ if declare
49
+ has_one_attr(attr_name, 'Eluvia::File')
50
+ end
51
+
52
+ # Register metadata
53
+ self._file_attrs[attr_name] = {
54
+ attr_name: attr_name,
55
+ underlying_attachment: underlying_attachment,
56
+ nature: nature
57
+ }
58
+
59
+ # *******************************************************************
60
+ # Dynamically defined instance methods for file
61
+ # *******************************************************************
62
+
63
+ if underlying_attachment
64
+
65
+ # Set method
66
+ define_method(:"#{attr_name}=") do |value|
67
+ # Delete the existing attachment if requested
68
+ if value.nil?
69
+ attachment_record = self.send(underlying_attachment)
70
+ attachment_record.purge_later if attachment_record.attached?
71
+ return
72
+ end
73
+
74
+ # Normalize and check value
75
+ value = Eluvia::File.map(value) if value.is_a?(::Hash)
76
+ raise Eluvia::Errors::BadRequest.new("Invalid value for file attribute `#{attr_name}`, expecting Eluvia::File.") unless value.is_a?(Eluvia::File)
77
+
78
+ # Retrieve and check filename - for a chunked/direct upload the frontend only sends
79
+ # `upload_key` (see habarico's `uploadFileChunked`), not a separate filename; the key
80
+ # itself already encodes the original filename as its last path segment (providers
81
+ # build it as `<tmp_prefix><uuid>/<filename>`), so fall back to that instead of
82
+ # requiring it to be sent twice.
83
+ filename = value.filename
84
+ filename = ::File.basename(value.upload_key) if filename.blank? && value.upload_key.present?
85
+ raise Eluvia::Errors::BadRequest.new("Expected `filename` in the attribute `#{attr_name}`.") if filename.blank?
86
+
87
+ # Sanitize filename
88
+ filename = ActiveStorage::Filename.new(filename).sanitized
89
+
90
+ # Retrieve the underlying attribute
91
+ attachment_record = self.send(underlying_attachment)
92
+
93
+ if value.upload_key.present?
94
+ # Finalize a previously uploaded (chunked/direct) temp object via the registered finalizer
95
+ finalizer = Eluvia::Uploads.finalizer
96
+ raise Eluvia::Errors::StandardError.new("No `Eluvia::Uploads.finalizer` registered, cannot finalize upload for attribute `#{attr_name}`.") unless finalizer.respond_to?(:call)
97
+ finalizer.call(attachment_record, value.upload_key, filename)
98
+ else
99
+ # Attach base64 encoded content directly
100
+ content = value.content
101
+ raise Eluvia::Errors::BadRequest.new("Expected `filename` and `content` in the attribute `#{attr_name}`.") if content.blank?
102
+ attachment_record.attach(
103
+ io: StringIO.new(Base64.decode64(content)),
104
+ filename: filename,
105
+ )
106
+ end
107
+ end
108
+
109
+ # Get method
110
+ define_method(attr_name) do
111
+ # Read value from underlying attribute
112
+ attachment = self.send(underlying_attachment)
113
+
114
+ # Handle blank value or not attached
115
+ return nil unless attachment&.attached?
116
+
117
+ # Convert to standard format
118
+ Eluvia::File.from_active_storage_attachment(attachment, skip_check: true)
119
+ end
120
+
121
+ else
122
+
123
+ # Set method
124
+ define_method(:"#{attr_name}=") do |value|
125
+ # Normalize value
126
+ value = Eluvia::File.map(value) if value.is_a?(::Hash)
127
+
128
+ # Check input
129
+ raise Eluvia::Errors::BadRequest.new("Invalid value for file attribute `#{attr_name}`, expecting Eluvia::File.") unless value.is_a?(Eluvia::File)
130
+
131
+ # Store to the underlying attribute
132
+ write_attribute(attr_name, value)
133
+ end
134
+
135
+ # Get method
136
+ define_method(attr_name) do
137
+ # Read value from underlying attribute
138
+ value = read_attribute(attr_name)
139
+
140
+ # Handle blank value
141
+ return nil if value.blank?
142
+
143
+ # Normalize value
144
+ value = Eluvia::File.map(value) if value.is_a?(::Hash)
145
+
146
+ # Check output
147
+ raise Eluvia::Errors::StandardError.new("Invalid value for object attribute `#{attr_name}`, expecting Eluvia::File.") unless value.is_a?(Eluvia::File)
148
+
149
+ value
150
+ end
151
+
152
+ end
153
+ end
154
+
155
+ end
156
+ end
157
+ end
158
+ end