eluvia-base 3.40.0 → 3.41.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcf7c769d60f4347beee0d63aff1e1e8944e5fb2412985f531465dd833cfe58e
4
- data.tar.gz: dc7491fcac983ef84e9f33396139370ce94fb65ce8718742acc4806d4e35e0b6
3
+ metadata.gz: 8d7c48fd8f44bcda78e3e702b68e2d1fc1da1f16c5c5231f9243c1a82c3132ca
4
+ data.tar.gz: 9eba98e2363832aea7e3015cfdc9312b7fb88fccae0d45def95cb34b8606a0ef
5
5
  SHA512:
6
- metadata.gz: 1a8eff124c578195160772b542e7ece4419b76ada79214f3b7d7ba3cb5c24768249b8d8c24ea207e37a33e6dc5d4d3b7196f83d369df428ae9724b36702af1e4
7
- data.tar.gz: 2df71bbb4195fde554b62bc8052d02894be13b14c652325f08af43b94d4b23385499bf0dbe5847ae16786519e59de04be8b6d8a65f4e734f8202bc99f793a55a
6
+ metadata.gz: eb92181c8003d8da9d2647734357877fc8e75aed3ae362ccd8fe91dad0da757f28648fbe1fffd9fcadb367ae97b5884670ae202847a5bf8842730b45b5355438
7
+ data.tar.gz: c6aa908b43d905ba1080d12b5c6c2cbeda73917c00a58ae45fbd3ef3c9ddf293ab87726036945ff27fce510dbac61d4ced691a1341d2997babeaa4c0c2e6b0d6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.41.0] — 2026-10-27
9
+
10
+ ### Added
11
+ - `Eluvia::ActiveRecord::FileArrayAttribute` registers each `file_array_attr` like `file_attr`
12
+ does: `file_array_attrs`, `file_array_attr_names`, `has_file_array_attr?` and
13
+ `find_file_array_attr` return its `underlying_attachments`, `underlying_filter`, `nature` and
14
+ `append_only` (HAB-991).
15
+ - `append_only:` option on `file_array_attr`: the setter takes a list of flat
16
+ `{filename, content}` files, attaches each one with content, never removes an existing
17
+ attachment, and ignores `nil`. Requires `underlying_attachments` (HAB-991).
18
+
8
19
  ## [3.40.0] — 2026-09-23
9
20
 
10
21
  ### Added
@@ -6,6 +6,30 @@ module Eluvia
6
6
 
7
7
  class_methods do
8
8
 
9
+ # *********************************************************************
10
+ # Attribute store methods
11
+ # *********************************************************************
12
+
13
+ def _file_array_attrs
14
+ @file_array_attrs ||= {}
15
+ end
16
+
17
+ def file_array_attrs
18
+ _file_array_attrs.values
19
+ end
20
+
21
+ def file_array_attr_names
22
+ file_array_attrs.map { |a| a[:attr_name] }
23
+ end
24
+
25
+ def has_file_array_attr?(attr_name)
26
+ _file_array_attrs.key?(attr_name.to_sym)
27
+ end
28
+
29
+ def find_file_array_attr(attr_name)
30
+ _file_array_attrs.fetch(attr_name.to_sym, nil)
31
+ end
32
+
9
33
  # *********************************************************************
10
34
  # Attribute definition methods for file array
11
35
  # *********************************************************************
@@ -16,78 +40,101 @@ module Eluvia
16
40
  # @param underlying_attachments [Symbol, nil] Name of the attribute holding underlying Active Storage attachments
17
41
  # @param underlying_filter [Proc, nil] Optional filter to be applied to scope underlying attachments
18
42
  # @param nature [Symbol] Nature of file attribute (:file, :image, etc.)
43
+ # @param append_only [Boolean] Whether the setter only attaches new `{filename, content}` files and never removes existing ones
19
44
  # @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)
45
+ def file_array_attr(attr_name, underlying_attachments: nil, underlying_filter: nil, nature: :file, append_only: false, declare: false)
21
46
  attr_name = attr_name.to_sym
22
47
  nature = nature.to_sym
23
48
 
49
+ raise ArgumentError, "`append_only` file array attribute `#{attr_name}` requires `underlying_attachments`." if append_only && !underlying_attachments
50
+
24
51
  # Define it using "has many" attribute
25
52
  if declare
26
53
  has_many_attr(attr_name, nature == :image ? 'Eluvia::ImageWrapper' : 'Eluvia::FileWrapper')
27
54
  end
28
55
 
56
+ # Register metadata
57
+ self._file_array_attrs[attr_name] = {
58
+ attr_name: attr_name,
59
+ underlying_attachments: underlying_attachments,
60
+ underlying_filter: underlying_filter,
61
+ nature: nature,
62
+ append_only: append_only
63
+ }
64
+
29
65
  # *******************************************************************
30
66
  # Dynamically defined instance methods for file array
31
67
  # *******************************************************************
32
68
 
33
69
  if underlying_attachments
34
70
 
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)
71
+ if append_only
72
+
73
+ # Set method - attaches each new file, leaves the existing attachments untouched
74
+ define_method(:"#{attr_name}=") do |new_values|
75
+ append_file_array_uploads(attr_name, underlying_attachments, new_values)
76
+ end
77
+
78
+ else
79
+
80
+ # Set method
81
+ define_method(:"#{attr_name}=") do |new_values|
82
+
83
+ # Check input
84
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}`, expecting Array.") unless new_values.is_a?(::Array)
85
+
86
+ # Current values
87
+ current_values = self.send(attr_name)
88
+ current_ids = current_values.map(&:id)
89
+ new_ids = []
90
+
91
+ # Create missing attachments
92
+ new_values.each do |value|
93
+ # Normalize and check value
94
+ if value.is_a?(::Hash)
95
+ if nature == :image
96
+ value = Eluvia::ImageWrapper.map(value)
97
+ else
98
+ value = Eluvia::FileWrapper.map(value)
99
+ end
54
100
  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?
101
+ 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)
102
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[].file`, can't be blank.") if value.file.blank?
58
103
 
59
- # Keep track of new ids
60
- new_ids << value.id if value.id
104
+ # Keep track of new ids
105
+ new_ids << value.id if value.id
61
106
 
62
- # Retrieve filename and content
63
- filename = value.file&.filename
64
- content = value.file&.content
107
+ # Retrieve filename and content
108
+ filename = value.file&.filename
109
+ content = value.file&.content
65
110
 
66
- # Sanitize filename
67
- filename = ActiveStorage::Filename.new(filename).sanitized
111
+ # Sanitize filename
112
+ filename = ActiveStorage::Filename.new(filename).sanitized
68
113
 
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?
114
+ # Create or update attachment only if both filename and content are passed (this means that user uploaded a new file)
115
+ if !filename.blank? && !content.blank?
71
116
 
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
117
+ # Delete the existing attachment
118
+ if value.id && current_ids.include?(value.id)
119
+ attachment_record = self.send("#{underlying_attachments}_attachments").find(value.id)
120
+ attachment_record.purge_later
121
+ end
122
+
123
+ # Create a new attachment
124
+ self.send(underlying_attachments).attach(io: StringIO.new(Base64.decode64(content)), filename: filename)
76
125
  end
77
126
 
78
- # Create a new attachment
79
- self.send(underlying_attachments).attach(io: StringIO.new(Base64.decode64(content)), filename: filename)
80
127
  end
81
128
 
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
129
+ # Delete no longer wanted attachments
130
+ current_values.each do |value|
131
+ unless new_ids.include?(value.id)
132
+ attachment_record = self.send("#{underlying_attachments}_attachments").find(value.id)
133
+ attachment_record.purge_later
134
+ end
89
135
  end
90
136
  end
137
+
91
138
  end
92
139
 
93
140
  # Get method
@@ -111,6 +158,32 @@ module Eluvia
111
158
  end
112
159
 
113
160
  end
161
+
162
+ private
163
+
164
+ # Attaches each `{filename, content}` upload of an append-only file array attribute
165
+ def append_file_array_uploads(attr_name, underlying_attachments, new_values)
166
+ return if new_values.nil?
167
+
168
+ # Check input
169
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}`, expecting Array.") unless new_values.is_a?(::Array)
170
+
171
+ new_values.each do |value|
172
+ # Normalize and check value
173
+ value = Eluvia::File.map(value) if value.is_a?(::Hash)
174
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[]`, expecting Eluvia::File.") unless value.is_a?(Eluvia::File)
175
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[]`, `upload_key` is not supported.") if value.upload_key.present?
176
+
177
+ # An item without content carries no new file
178
+ next if value.content.blank?
179
+
180
+ filename = ActiveStorage::Filename.new(value.filename.to_s).sanitized
181
+ raise Eluvia::Errors::BadRequest.new("Invalid value for attribute `#{attr_name}[].filename`, can't be blank.") if filename.blank?
182
+
183
+ self.send(underlying_attachments).attach(io: StringIO.new(Base64.decode64(value.content)), filename: filename)
184
+ end
185
+ end
186
+
114
187
  end
115
188
  end
116
189
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eluvia-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.40.0
4
+ version: 3.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matěj Outlý
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-23 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client