model_driven_api 3.11.4 → 3.12.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: 3f561a2d22648c4e007dfc73788277d525bff52b8b441bcafc030d96be12034b
4
- data.tar.gz: c4aab8c26319cfe1988b8fd7ac889c8d5c6b975e4e58eca31a0f566767f017eb
3
+ metadata.gz: '0877810a34bb2f213343f3e5f13bfee52b0cec2d8da29431c64466185078d01b'
4
+ data.tar.gz: d52e6f70ae1da5655ab7b5ad605c400c88ed659d84425b91e5a239506d2d9d6a
5
5
  SHA512:
6
- metadata.gz: 2a50be7ab0901fcd345403ce5fa46c6a98446303be27fd380dc4202e37366fe86dc51bbb06dc8a9d9c9350bec20e95938e0ff1ecaa5d25006f2c2dfea53c2491
7
- data.tar.gz: bda7a791bc646f8326ca00efd5ae04b9df7dd02e1f36ffb8e8edbcd40055e24034e47d76594f5f5aae4838ddbe17699f33eb85fbe284d5bf3e538df5f035a1d9
6
+ metadata.gz: 2a557cb8c023b0e98fc299c3c2334e6720faa58f2383f8f207ad53a5d00f6b66a8b92c9756d467aed8b02a21fc0beb145c5eb92d3fe20ca226de2963fe4ef7ab
7
+ data.tar.gz: eccaf19b8ecdf2c13283c3918ec4be6d1d8c27d4e52b1e37490ddae532720be75a03a11853f882bb14778c141198cb123f2c610a518d3397b351157e7de523b9
@@ -75,8 +75,7 @@ class Api::V2::ApplicationController < ActionController::API
75
75
  # TODO: Remove it
76
76
  # @record.user_id = current_user.id if @model.column_names.include? "user_id"
77
77
  @record = @model.new(@body)
78
- @record.save!
79
- render json: @record.to_json(json_attrs), status: 201
78
+ create_once(@record) { render json: @record.to_json(json_attrs), status: 201 }
80
79
  end
81
80
 
82
81
  def update
@@ -210,6 +209,46 @@ class Api::V2::ApplicationController < ActionController::API
210
209
  return not_found! if @record.blank?
211
210
  end
212
211
 
212
+ # A model may declare `self.creation_key`: the columns identifying a record its client
213
+ # generated. A create whose key matches an existing record is a repeat of a creation that
214
+ # already happened (a client resending after a lost response), so the caller gets that
215
+ # record back instead of a copy.
216
+ def existing_by_creation_key(record)
217
+ return unless @model.respond_to?(:creation_key)
218
+
219
+ key = @model.creation_key.index_with { |column| record[column] }
220
+ return if key.values.any?(&:nil?)
221
+
222
+ @model.find_by(key)
223
+ end
224
+
225
+ # Saves +record+ and yields to render it, unless its creation key matches an existing
226
+ # record, which is rendered instead (render_existing). Shared by v2 and v3, which differ
227
+ # only in how they render.
228
+ def create_once(record)
229
+ existing = existing_by_creation_key(record)
230
+ return render_existing(existing) if existing
231
+
232
+ # In its own savepoint, so a unique-index violation leaves any enclosing transaction
233
+ # usable for the lookup that follows it.
234
+ @model.transaction(requires_new: true) { record.save! }
235
+ yield
236
+ rescue ActiveRecord::RecordNotUnique
237
+ # A racing request with the same creation key committed between the lookup and the
238
+ # insert: the unique index stopped this one, so hand back what the other created.
239
+ existing = existing_by_creation_key(record)
240
+ raise unless existing
241
+
242
+ render_existing(existing)
243
+ end
244
+
245
+ # Only to a caller who could read it anyway (the same check as show): a key is no
246
+ # credential, so guessing someone else's must not hand their record over.
247
+ def render_existing(existing)
248
+ authorize! :show, existing
249
+ render json: existing.to_json(json_attrs), status: 200
250
+ end
251
+
213
252
  def json_attrs
214
253
  # In order of importance: if you send the configuration via querystring you are ok
215
254
  # has precedence over if you have setup the json_attrs in the model concern
@@ -34,9 +34,10 @@ class Api::V3::ApplicationController < Api::V2::ApplicationController
34
34
  return render json: result, status: (status_number.presence || 200) if status == true
35
35
 
36
36
  record = @model.new(jsonapi_attributes)
37
- record.save!
38
- serializer = Api::V3::SerializerFactory.serializer_for(@model)
39
- render json: serializer.new(record, **serializer_opts).serializable_hash, status: :created
37
+ create_once(record) do
38
+ serializer = Api::V3::SerializerFactory.serializer_for(@model)
39
+ render json: serializer.new(record, **serializer_opts).serializable_hash, status: :created
40
+ end
40
41
  end
41
42
 
42
43
  def update
@@ -64,6 +65,13 @@ class Api::V3::ApplicationController < Api::V2::ApplicationController
64
65
 
65
66
  private
66
67
 
68
+ # v2's read check, with a JSON:API envelope.
69
+ def render_existing(existing)
70
+ authorize! :show, existing
71
+ serializer = Api::V3::SerializerFactory.serializer_for(@model)
72
+ render json: serializer.new(existing, **serializer_opts).serializable_hash, status: :ok
73
+ end
74
+
67
75
  def apply_filters(scope)
68
76
  filter_params = params[:filter] || {}
69
77
  filter_params.each do |field, value|
@@ -19,7 +19,9 @@ module ApiExceptionManagement
19
19
  # #destroy! — reuse invalid! since both exceptions expose the same .record interface.
20
20
  rescue_from ActiveRecord::RecordNotDestroyed, with: :invalid!
21
21
  rescue_from ActiveRecord::RecordNotFound, with: :not_found!
22
- rescue_from ActiveRecord::RecordNotUnique, with: :invalid!
22
+ # Not invalid!: RecordNotUnique carries no .record, so invalid! itself raised and
23
+ # every unique-index violation answered 500.
24
+ rescue_from ActiveRecord::RecordNotUnique, with: :not_unique!
23
25
  # Rescue Stale Object in Optimistick locking with stale!
24
26
  rescue_from ActiveRecord::StaleObjectError, with: :stale!
25
27
  rescue_from EndpointValidationError, with: :endpoint_invalid!
@@ -51,6 +53,13 @@ module ApiExceptionManagement
51
53
  return api_error status: 422, errors: exception.record.errors
52
54
  end
53
55
 
56
+ # The database message names tables, columns and index definitions: log it, don't
57
+ # send it back.
58
+ def not_unique! exception = ActiveRecord::RecordNotUnique.new
59
+ Rails.logger.warn("RecordNotUnique: #{exception.message}")
60
+ return api_error status: 422, errors: "A record with the same unique values already exists"
61
+ end
62
+
54
63
  def fivehundred! exception = StandardError.new
55
64
  return api_error status: 500, errors: exception.message
56
65
  end
@@ -1,3 +1,3 @@
1
1
  module ModelDrivenApi
2
- VERSION = "3.11.4".freeze
2
+ VERSION = "3.12.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model_driven_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.4
4
+ version: 3.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriele Tassoni