• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gregschmit / rails-rest-framework / 24620147265

19 Apr 2026 03:35AM UTC coverage: 87.62% (+0.001%) from 87.619%
24620147265

push

github

gregschmit
Initial work to allow both per-record and single-query bulk ops.

102 of 114 new or added lines in 4 files covered. (89.47%)

5 existing lines in 2 files now uncovered.

1189 of 1357 relevant lines covered (87.62%)

213.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.6
/lib/rest_framework/controller/bulk.rb
1
module RESTFramework::Controller
2✔
2
  # Serialize the records, but also include any errors that might exist.
3
  def bulk_serialize(records)
2✔
4
    # This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
5
    # the serializer directly. This would fail for active model serializers, but maybe we don't
6
    # care?
7
    s = RESTFramework::Utils.wrap_ams(self.get_serializer_class)
12✔
8
    records.map do |record|
12✔
9
      s.new(record, controller: self).serialize.merge!({ errors: record.errors.presence }.compact)
24✔
10
    end
11
  end
12

13
  # Resolve the effective bulk mode (:default or :raw) for this request.
14
  def get_bulk_mode
2✔
15
    return @_get_bulk_mode if defined?(@_get_bulk_mode)
56✔
16

17
    # If mode override is allowed, check the query param.
18
    if self.class.bulk_allow_mode_override && (qp = self.class.bulk_mode_query_param)
28✔
NEW
19
      if (requested = params[qp].presence)
×
NEW
20
        requested = requested.to_sym
×
NEW
21
        unless requested.in?([ :default, :raw ])
×
NEW
22
          raise RESTFramework::InvalidBulkParametersError.new(
×
23
            "Invalid bulk mode: #{requested}. Must be `default` or `raw`.",
24
          )
25
        end
NEW
26
        return @_get_bulk_mode = requested
×
27
      end
28
    end
29

30
    # Normalize: `true` and `:default` both mean per-record processing.
31
    @_get_bulk_mode = self.class.bulk == :raw ? :raw : :default
28✔
32
  end
33

34
  # Resolve whether partial fulfillment is enabled for this request.
35
  def get_bulk_partial
2✔
36
    return @_get_bulk_partial if defined?(@_get_bulk_partial)
20✔
37

38
    # Check the query param first if configured.
39
    if (qp = self.class.bulk_partial_query_param)
20✔
40
      if (requested = params[qp].presence)
20✔
41
        return @_get_bulk_partial = ActiveModel::Type::Boolean.new.cast(requested)
6✔
42
      end
43
    end
44

45
    @_get_bulk_partial = self.class.bulk_partial
14✔
46
  end
47

48
  # Validate and extract bulk object data from request parameters.
49
  def _bulk_object_data(bulk_action)
2✔
50
    data = self.get_body_params(bulk_action: bulk_action)[:_json]
20✔
51

52
    unless data&.is_a?(Array) && data.all? { |r| r.is_a?(ActionController::Parameters) }
60✔
53
      raise RESTFramework::InvalidBulkParametersError.new("Expected an array of objects.")
×
54
    end
55

56
    # Enforce size limits.
57
    max = self.get_bulk_mode == :raw ? self.class.bulk_max_raw_size : self.class.bulk_max_size
20✔
58
    if max && data.length > max
20✔
NEW
59
      raise RESTFramework::InvalidBulkParametersError.new(
×
60
        "Too many records (#{data.length}); maximum is #{max}.",
61
      )
62
    end
63

64
    data
20✔
65
  end
66

67
  # Validate and extract bulk primary key data from request parameters.
68
  def _bulk_pk_data
2✔
69
    data = self.get_destroy_params(bulk_action: :destroy)[:_json]
8✔
70

71
    unless data&.is_a?(Array) && data.all? { |r| r.is_a?(String) || r.is_a?(Numeric) }
24✔
NEW
72
      raise RESTFramework::InvalidBulkParametersError.new("Expected an array of primary keys.")
×
73
    end
74

75
    # Enforce size limits.
76
    max = self.get_bulk_mode == :raw ? self.class.bulk_max_raw_size : self.class.bulk_max_size
8✔
77
    if max && data.length > max
8✔
NEW
78
      raise RESTFramework::InvalidBulkParametersError.new(
×
79
        "Too many records (#{data.length}); maximum is #{max}.",
80
      )
81
    end
82

83
    data
8✔
84
  end
85

86
  # --- Create ---
87

88
  def create_all
2✔
89
    if self.get_bulk_mode == :raw
12✔
90
      result = self.create_all_raw!
4✔
91
      return render(api: { result: result })
2✔
92
    end
93

94
    records = self.create_all_record!
8✔
95
    render(api: self.bulk_serialize(records), status: :created)
4✔
96
  end
97

98
  def create_all!
2✔
NEW
99
    self.get_bulk_mode == :raw ? self.create_all_raw! : self.create_all_record!
×
100
  end
101

102
  def create_all_raw!
2✔
103
    pk = self.class.model.primary_key
4✔
104
    data = self._bulk_object_data(:create)
4✔
105

106
    unless first_keys = data.first&.keys&.sort
4✔
107
      raise RESTFramework::InvalidBulkParametersError.new("Expected objects with attrs.")
×
108
    end
109
    unless data.all? { |r| r.keys.sort == first_keys }
12✔
110
      raise RESTFramework::InvalidBulkParametersError.new("All objects must have the same attrs.")
×
111
    end
112

113
    self.create_from.insert_all(data, unique_by: pk)
4✔
114
  end
115

116
  def create_all_record!
2✔
117
    data = self._bulk_object_data(:create)
8✔
118
    collection = self.create_from
8✔
119

120
    if self.get_bulk_partial
8✔
121
      # Partial: save each record individually, return all (some may have errors).
122
      data.map { |attrs| collection.create(attrs) }
6✔
123
    else
124
      # Transactional: validate all first, then save in a transaction or raise.
125
      records = data.map { |attrs| collection.new(attrs) }
18✔
126
      failed = records.reject(&:valid?)
6✔
127

128
      if failed.any?
6✔
129
        raise RESTFramework::BulkRecordErrorsError.new(records)
2✔
130
      end
131

132
      self.class.model.transaction do
4✔
133
        records.each(&:save!)
4✔
134
      end
135

136
      records
2✔
137
    end
138
  end
139

140
  # --- Update ---
141

142
  def update_all
2✔
143
    if self.get_bulk_mode == :raw
8✔
144
      result = self.update_all_raw!
2✔
145
      return render(api: { result: result })
2✔
146
    end
147

148
    records = self.update_all_record!
6✔
149
    render(api: self.bulk_serialize(records))
4✔
150
  end
151

152
  def update_all!
2✔
NEW
153
    self.get_bulk_mode == :raw ? self.update_all_raw! : self.update_all_record!
×
154
  end
155

156
  def update_all_raw!
2✔
157
    pk = self.class.model.primary_key
2✔
158
    data = self._bulk_object_data(:update)
2✔
159

160
    data_ids = data.map { |r| r[pk] }.uniq
6✔
161
    if self.get_recordset.where(pk => data_ids).count != data_ids.length
2✔
162
      raise RESTFramework::InvalidBulkParametersError.new("Some objects not found.")
×
163
    end
164

165
    unless first_keys = data.first&.keys&.sort
2✔
166
      raise RESTFramework::InvalidBulkParametersError.new("Expected objects with attrs.")
×
167
    end
168
    unless data.all? { |r| r.keys.sort == first_keys }
6✔
UNCOV
169
      raise RESTFramework::InvalidBulkParametersError.new("All objects must have the same attrs.")
×
170
    end
171

172
    self.get_recordset.upsert_all(data, unique_by: pk)
2✔
173
  end
174

175
  def update_all_record!
2✔
176
    pk = self.class.model.primary_key
6✔
177
    data = self._bulk_object_data(:update)
6✔
178

179
    # Look up all records in one query.
180
    data_ids = data.map { |r| r[pk] }.uniq
18✔
181
    existing = self.get_recordset.where(pk => data_ids).index_by { |r| r.send(pk) }
18✔
182

183
    if existing.length != data_ids.length
6✔
NEW
184
      raise RESTFramework::InvalidBulkParametersError.new("Some objects not found.")
×
185
    end
186

187
    # Assign attributes to each record.
188
    records = data.map { |attrs|
6✔
189
      record = existing[attrs[pk]]
12✔
190
      record.assign_attributes(attrs.except(pk))
12✔
191
      record
12✔
192
    }
193

194
    if self.get_bulk_partial
6✔
195
      # Partial: save each record individually.
196
      records.each { |r| r.save }
6✔
197
      records
2✔
198
    else
199
      # Transactional: validate all first, then save in a transaction or raise.
200
      failed = records.reject(&:valid?)
4✔
201

202
      if failed.any?
4✔
203
        raise RESTFramework::BulkRecordErrorsError.new(records)
2✔
204
      end
205

206
      self.class.model.transaction do
2✔
207
        records.each(&:save!)
2✔
208
      end
209

210
      records
2✔
211
    end
212
  end
213

214
  # --- Destroy ---
215

216
  def destroy_all
2✔
217
    if self.get_bulk_mode == :raw
8✔
218
      deleted = self.destroy_all_raw!
2✔
219
      return render(api: { result: deleted })
2✔
220
    end
221

222
    records = self.destroy_all_record!
6✔
223
    render(api: self.bulk_serialize(records))
4✔
224
  end
225

226
  def destroy_all!
2✔
NEW
227
    self.get_bulk_mode == :raw ? self.destroy_all_raw! : self.destroy_all_record!
×
228
  end
229

230
  def destroy_all_raw!
2✔
231
    data = self._bulk_pk_data
2✔
232
    pk = self.class.model.primary_key
2✔
233
    self.get_recordset.where(pk => data).delete_all
2✔
234
  end
235

236
  def destroy_all_record!
2✔
237
    data = self._bulk_pk_data
6✔
238
    pk = self.class.model.primary_key
6✔
239
    records = self.get_recordset.where(pk => data).to_a
6✔
240

241
    if self.get_bulk_partial
6✔
242
      # Partial: destroy each record individually.
243
      records.each { |r| r.destroy }
6✔
244
      records
2✔
245
    else
246
      # Transactional: destroy all in a transaction, roll back on failure.
247
      self.class.model.transaction do
4✔
248
        records.each(&:destroy!)
4✔
249
      end
250

251
      records
2✔
252
    end
253
  end
254
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc