labimotion 2.2.0.rc8 → 2.2.0.rc10

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.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # LabIMotion [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3755759.svg)](https://doi.org/10.5281/zenodo.8305411)
2
+
3
+ LabIMotion is a software that offers the flexibility to design new modules tailored to the specific needs of the scientists.
4
+ Generic elements, segments, and datasets are organized using Components, which are introduced as layers and fields. Each generic element, segment, or dataset can include multiple layers, with the potential for multiple fields within each layer. This hierarchical structure enables a flexible and comprehensive organization of data and information.
5
+
6
+
7
+
8
+ ![Design Principles](https://www.chemotion.net/assets/images/generic_feature_outline-a58eee8e02ca7247e54f7ad17ee2c102.png)
9
+
10
+
11
+ ## Version 1.0.18 of LabIMotion, featuring:
12
+
13
+ * Generic Designer
14
+ * Workflow of Generic Element
15
+ * Repetitation of layers
16
+ * Drag Element to Element
17
+ * Dataset Metadata
18
+ * LabIMotion Template Hub Synchronization [**[LabIMotion Template Hub]**]
19
+
20
+ ---
21
+
22
+ This repository contains a backend service for the LabIMotion. It is written in **[Ruby]**.
23
+
24
+ ### Community
25
+
26
+ * [GitHub discussions](https://github.com/labimotion/labimotion/discussions)
27
+
28
+ ### Code
29
+
30
+ * [GitHub code](https://github.com/labimotion/labimotion) and [bug tracker](https://github.com/labimotion/labimotion/issues)
31
+
32
+ ---
33
+
34
+
35
+ ## Documentation
36
+
37
+ Documentation for users **⸢ [Documentation] ⸥**
38
+
39
+ Documentation for developers **⸢ [Technical Documentation] ⸥**
40
+
41
+ ---
42
+
43
+ ## License
44
+
45
+ Code released under the [AGPL-3.0 License]([https://www.gnu.org/licenses/agpl-3.0.txt](https://www.gnu.org/licenses/agpl-3.0.txt)).
46
+
47
+ ---
48
+
49
+ ## Acknowledgments
50
+
51
+ This project has been funded by the **[DFG]**.
52
+
53
+ [![DFG Logo]][DFG]
54
+
55
+
56
+ Funded by the [Deutsche Forschungsgemeinschaft (DFG, German Research Foundation)](https://www.dfg.de/) under the [National Research Data Infrastructure – NFDI4Chem](https://nfdi4chem.de/) – Projektnummer **441958208** since 2020.
57
+
58
+
59
+
60
+ <!----------------------------------------------------------------------------->
61
+ [Documentation]: https://www.chemotion.net/docs/labimotion/
62
+ [Technical Documentation]: https://www.rubydoc.info/gems/labimotion
63
+ [DFG]: https://www.dfg.de/en/
64
+ [DFG Logo]: https://www.dfg.de/zentralablage/bilder/service/logos_corporate_design/logo_negativ_267.png
65
+ [Nicole Jung]: mailto:nicole.jung@kit.edu
66
+ [Karlsruhe Institute of Technology]: https://www.kit.edu/english/
67
+ [Ruby]: https://www.ruby-lang.org/
68
+ [LabIMotion Template Hub]: https://www.chemotion-repository.net/home/genericHub
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'labimotion/version'
4
+
5
+ module Labimotion
6
+ # Dose Response Request API
7
+ class DoseRespRequestAPI < Grape::API
8
+ helpers Labimotion::ParamHelpers
9
+
10
+ resource :dose_resp_requests do
11
+ desc 'Get all dose response requests for current user'
12
+ params do
13
+ optional :element_id, type: Integer, desc: 'Filter by element ID'
14
+ optional :state, type: Integer, desc: 'Filter by state (-1, 0, 1, 2)'
15
+ optional :page, type: Integer, desc: 'Page number', default: 1
16
+ optional :per_page, type: Integer, desc: 'Items per page', default: 20
17
+ end
18
+ get do
19
+ requests = Labimotion::DoseRespRequest.where(created_by: current_user.id)
20
+
21
+ # Apply filters
22
+ requests = requests.where(element_id: params[:element_id]) if params[:element_id]
23
+ requests = requests.where(state: params[:state]) if params[:state]
24
+
25
+ # Pagination
26
+ page = params[:page] || 1
27
+ per_page = [params[:per_page] || 20, 100].min # Max 100 per page
28
+
29
+ total = requests.count
30
+ requests = requests.order(created_at: :desc)
31
+ .offset((page - 1) * per_page)
32
+ .limit(per_page)
33
+
34
+ {
35
+ requests: requests.map do |req|
36
+ {
37
+ id: req.id,
38
+ request_id: req.request_id,
39
+ element_id: req.element_id,
40
+ state: req.state,
41
+ state_label: state_label(req.state),
42
+ expires_at: req.expires_at,
43
+ created_at: req.created_at,
44
+ updated_at: req.updated_at,
45
+ first_accessed_at: req.first_accessed_at,
46
+ last_accessed_at: req.last_accessed_at,
47
+ access_count: req.access_count,
48
+ resp_message: req.resp_message,
49
+ active: req.active?
50
+ }
51
+ end,
52
+ pagination: {
53
+ page: page,
54
+ per_page: per_page,
55
+ total: total,
56
+ total_pages: (total.to_f / per_page).ceil
57
+ }
58
+ }
59
+ rescue StandardError => e
60
+ error!("Error: #{e.message}", 500)
61
+ end
62
+
63
+ desc 'Get a dose response request by ID'
64
+ params do
65
+ requires :id, type: Integer, desc: 'Request ID'
66
+ end
67
+ get ':id' do
68
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
69
+ error!('Request not found', 404) unless request
70
+
71
+ # Check authorization
72
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
73
+
74
+ {
75
+ id: request.id,
76
+ request_id: request.request_id,
77
+ element_id: request.element_id,
78
+ state: request.state,
79
+ state_label: state_label(request.state),
80
+ wellplates_metadata: request.wellplates_metadata,
81
+ input_metadata: request.input_metadata,
82
+ expires_at: request.expires_at,
83
+ revoked_at: request.revoked_at,
84
+ created_at: request.created_at,
85
+ updated_at: request.updated_at,
86
+ first_accessed_at: request.first_accessed_at,
87
+ last_accessed_at: request.last_accessed_at,
88
+ access_count: request.access_count,
89
+ resp_message: request.resp_message,
90
+ active: request.active?,
91
+ expired: request.expired?,
92
+ revoked: request.revoked?
93
+ }
94
+ rescue StandardError => e
95
+ error!("Error: #{e.message}", 500)
96
+ end
97
+
98
+ desc 'Update a dose response request'
99
+ params do
100
+ requires :id, type: Integer, desc: 'Request ID'
101
+ optional :state, type: Integer, desc: 'State', values: [-1, 0, 1, 2]
102
+ optional :resp_message, type: String, desc: 'Response message'
103
+ optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
104
+ end
105
+ put ':id' do
106
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
107
+ error!('Request not found', 404) unless request
108
+
109
+ # Check authorization
110
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
111
+
112
+ update_params = {}
113
+ update_params[:state] = params[:state] if params[:state]
114
+ update_params[:resp_message] = params[:resp_message] if params[:resp_message]
115
+ update_params[:wellplates_metadata] = params[:wellplates_metadata] if params[:wellplates_metadata]
116
+
117
+ request.update!(update_params)
118
+
119
+ {
120
+ success: true,
121
+ message: 'Request updated successfully',
122
+ request: {
123
+ id: request.id,
124
+ request_id: request.request_id,
125
+ state: request.state,
126
+ state_label: state_label(request.state),
127
+ resp_message: request.resp_message,
128
+ updated_at: request.updated_at
129
+ }
130
+ }
131
+ rescue ActiveRecord::RecordInvalid => e
132
+ error!("Validation error: #{e.message}", 422)
133
+ rescue StandardError => e
134
+ error!("Error: #{e.message}", 500)
135
+ end
136
+
137
+ desc 'Revoke a dose response request'
138
+ params do
139
+ requires :id, type: Integer, desc: 'Request ID'
140
+ end
141
+ post ':id/revoke' do
142
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
143
+ error!('Request not found', 404) unless request
144
+
145
+ # Check authorization
146
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
147
+
148
+ request.revoke!
149
+
150
+ {
151
+ success: true,
152
+ message: 'Request revoked successfully',
153
+ request: {
154
+ id: request.id,
155
+ request_id: request.request_id,
156
+ state: request.state,
157
+ revoked_at: request.revoked_at,
158
+ active: request.active?
159
+ }
160
+ }
161
+ rescue StandardError => e
162
+ error!("Error: #{e.message}", 500)
163
+ end
164
+
165
+ desc 'Delete a dose response request'
166
+ params do
167
+ requires :id, type: Integer, desc: 'Request ID'
168
+ end
169
+ delete ':id' do
170
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
171
+ error!('Request not found', 404) unless request
172
+
173
+ # Check authorization
174
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
175
+
176
+ # Soft delete if acts_as_paranoid is enabled
177
+ request.destroy
178
+
179
+ {
180
+ success: true,
181
+ message: 'Request deleted successfully'
182
+ }
183
+ rescue StandardError => e
184
+ error!("Error: #{e.message}", 500)
185
+ end
186
+
187
+ desc 'Get dose response requests by element ID'
188
+ params do
189
+ requires :element_id, type: Integer, desc: 'Element ID'
190
+ end
191
+ get 'by_element/:element_id' do
192
+ element = Labimotion::Element.find_by(id: params[:element_id])
193
+ error!('Element not found', 404) unless element
194
+
195
+ # Check if user has access to element
196
+ policy = ElementPolicy.new(current_user, element)
197
+ error!('Unauthorized', 403) unless policy.read?
198
+
199
+ requests = Labimotion::DoseRespRequest.where(element_id: params[:element_id])
200
+ .where(created_by: current_user.id)
201
+ .order(created_at: :desc)
202
+
203
+ {
204
+ element_id: element.id,
205
+ element_name: element.name,
206
+ requests: requests.map do |req|
207
+ {
208
+ id: req.id,
209
+ request_id: req.request_id,
210
+ state: req.state,
211
+ state_label: state_label(req.state),
212
+ expires_at: req.expires_at,
213
+ created_at: req.created_at,
214
+ access_count: req.access_count,
215
+ active: req.active?
216
+ }
217
+ end
218
+ }
219
+ rescue StandardError => e
220
+ error!("Error: #{e.message}", 500)
221
+ end
222
+ end
223
+
224
+ helpers do
225
+ def state_label(state)
226
+ case state
227
+ when Labimotion::DoseRespRequest::STATE_ERROR
228
+ 'error'
229
+ when Labimotion::DoseRespRequest::STATE_INITIAL
230
+ 'initial'
231
+ when Labimotion::DoseRespRequest::STATE_PROCESSING
232
+ 'processing'
233
+ when Labimotion::DoseRespRequest::STATE_COMPLETED
234
+ 'completed'
235
+ else
236
+ 'unknown'
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementVariationAPI < Grape::API
5
+ rescue_from ActiveRecord::RecordNotFound do
6
+ error!('404 Element not found', 404)
7
+ end
8
+
9
+ resource :element_variations do
10
+ params do
11
+ requires :element_id, type: Integer, desc: 'Generic element id'
12
+ end
13
+ route_param :element_id do
14
+ before do
15
+ @element = Labimotion::Element.find(params[:element_id])
16
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).read?
17
+ end
18
+
19
+ desc 'Return element variations for a generic element'
20
+ get do
21
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
22
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
23
+ end
24
+
25
+ desc 'Upsert element variations for a generic element'
26
+ params do
27
+ requires :variations, type: Hash, desc: 'Variations keyed by row uuid'
28
+ optional :layout, type: Hash, desc: 'Column layout (selected/order/units/rowOrder)'
29
+ end
30
+ put do
31
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).update?
32
+
33
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
34
+ record.variations = params[:variations] || {}
35
+ if params.key?(:layout) && record.class.column_names.include?('layout')
36
+ record.layout = params[:layout] || {}
37
+ end
38
+ record.save!
39
+
40
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -179,10 +179,11 @@ module Labimotion
179
179
  end
180
180
 
181
181
  namespace :klass_revisions do
182
- desc 'list Generic Element Revisions'
182
+ desc 'list Generic Klass Revisions'
183
183
  params do
184
184
  requires :id, type: Integer, desc: 'Generic Element Klass Id'
185
185
  requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
186
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
186
187
  end
187
188
  get do
188
189
  list = list_klass_revisions(params)
@@ -197,6 +198,7 @@ module Labimotion
197
198
  desc 'list Generic Element Revisions'
198
199
  params do
199
200
  requires :id, type: Integer, desc: 'Generic Element Id'
201
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
200
202
  end
201
203
  get do
202
204
  list = element_revisions(params)
@@ -241,14 +243,15 @@ module Labimotion
241
243
  end
242
244
 
243
245
  namespace :segment_revisions do
244
- desc 'list Generic Element Revisions'
246
+ desc 'list Generic Segment Revisions'
245
247
  params do
246
248
  optional :id, type: Integer, desc: 'Generic Element Id'
249
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
247
250
  end
248
251
  get do
249
252
  klass = Labimotion::Segment.find(params[:id])
250
253
  list = klass.segments_revisions unless klass.nil?
251
- present list&.order(created_at: :desc)&.limit(10), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
254
+ present list&.order(created_at: :desc)&.limit(params[:limit]), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
252
255
  rescue StandardError => e
253
256
  Labimotion.log_exception(e, current_user)
254
257
  []
@@ -12,5 +12,9 @@ module Labimotion
12
12
  mount Labimotion::LabimotionHubAPI
13
13
  mount Labimotion::StandardLayerAPI
14
14
  mount Labimotion::VocabularyAPI
15
+ mount Labimotion::UserAPI
16
+ mount Labimotion::MttAPI
17
+ mount Labimotion::DoseRespRequestAPI
18
+ mount Labimotion::ElementVariationAPI
15
19
  end
16
20
  end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'labimotion/version'
4
+ require 'labimotion/libs/export_element'
5
+ require 'labimotion/helpers/mtt_helpers'
6
+ require 'labimotion/models/dose_resp_output'
7
+
8
+ module Labimotion
9
+ # Generic Element API
10
+ class MttAPI < Grape::API
11
+ helpers Labimotion::ParamHelpers
12
+ helpers Labimotion::MttHelpers
13
+
14
+ namespace :public do
15
+ resource :mtt_apps do
16
+ route_param :token do
17
+ desc 'Download data from MTT app (GET endpoint)'
18
+ get do
19
+ download_json_to_external_app
20
+ end
21
+
22
+ desc 'Upload modified data from MTT app (POST endpoint)'
23
+ post do
24
+ upload_json_from_external_app
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ resource :mtt do
31
+ namespace :requests do
32
+ desc 'Get MTT requests for current user'
33
+ get do
34
+ # Get all requests created by current user
35
+ requests = Labimotion::DoseRespRequest
36
+ .includes(:dose_resp_outputs)
37
+ .where(created_by: current_user.id)
38
+ .order(created_at: :desc)
39
+
40
+ # Return formatted response
41
+ requests.map do |req|
42
+ {
43
+ id: req.id,
44
+ request_id: req.request_id,
45
+ element_id: req.element_id,
46
+ state: req.state,
47
+ state_name: case req.state
48
+ when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
49
+ when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
50
+ when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
51
+ when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
52
+ else 'unknown'
53
+ end,
54
+ created_at: req.created_at,
55
+ expires_at: req.expires_at,
56
+ expired: req.expired?,
57
+ revoked: req.revoked?,
58
+ active: req.active?,
59
+ resp_message: req.resp_message,
60
+ last_accessed_at: req.last_accessed_at,
61
+ access_count: req.access_count || 0,
62
+ outputs: req.dose_resp_outputs.map do |output|
63
+ {
64
+ id: output.id,
65
+ output_data: output.output_data,
66
+ notes: output.notes,
67
+ created_at: output.created_at
68
+ }
69
+ end
70
+ }
71
+ end
72
+ end
73
+
74
+ desc 'Delete one or multiple MTT requests'
75
+ params do
76
+ requires :ids, type: Array[Integer], desc: 'Array of request IDs to delete'
77
+ end
78
+ delete do
79
+ # Find requests belonging to current user
80
+ requests = Labimotion::DoseRespRequest.where(
81
+ id: params[:ids],
82
+ created_by: current_user.id
83
+ )
84
+
85
+ if requests.empty?
86
+ error!('No requests found or unauthorized', 404)
87
+ end
88
+
89
+ deleted_count = requests.count
90
+ requests.destroy_all
91
+
92
+ {
93
+ success: true,
94
+ message: "Successfully deleted #{deleted_count} request(s)",
95
+ deleted_count: deleted_count
96
+ }
97
+ rescue StandardError => e
98
+ error!("Error deleting requests: #{e.message}", 500)
99
+ end
100
+
101
+ desc 'Update MTT request'
102
+ params do
103
+ requires :id, type: Integer, desc: 'Request ID'
104
+ optional :state, type: Integer, desc: 'State (-1: error, 0: initial, 1: processing, 2: completed)', values: [-1, 0, 1, 2]
105
+ optional :resp_message, type: String, desc: 'Response message'
106
+ optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
107
+ optional :input_metadata, type: Hash, desc: 'Input metadata'
108
+ optional :revoked, type: Boolean, desc: 'Revoke access token'
109
+ end
110
+ patch ':id' do
111
+ # Find request belonging to current user
112
+ request = Labimotion::DoseRespRequest.find_by(
113
+ id: params[:id],
114
+ created_by: current_user.id
115
+ )
116
+
117
+ error!('Request not found or unauthorized', 404) unless request
118
+
119
+ # Prepare update attributes
120
+ update_attrs = {}
121
+ update_attrs[:state] = params[:state] if params.key?(:state)
122
+ update_attrs[:resp_message] = params[:resp_message] if params.key?(:resp_message)
123
+ update_attrs[:wellplates_metadata] = params[:wellplates_metadata] if params.key?(:wellplates_metadata)
124
+ update_attrs[:input_metadata] = params[:input_metadata] if params.key?(:input_metadata)
125
+ update_attrs[:revoked_at] = params[:revoked] ? Time.current : nil if params.key?(:revoked)
126
+
127
+ if request.update(update_attrs)
128
+ {
129
+ success: true,
130
+ message: 'Request updated successfully',
131
+ request: {
132
+ id: request.id,
133
+ request_id: request.request_id,
134
+ state: request.state,
135
+ state_name: case request.state
136
+ when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
137
+ when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
138
+ when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
139
+ when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
140
+ else 'unknown'
141
+ end,
142
+ resp_message: request.resp_message,
143
+ revoked: request.revoked?,
144
+ updated_at: request.updated_at
145
+ }
146
+ }
147
+ else
148
+ error!("Validation error: #{request.errors.full_messages.join(', ')}", 422)
149
+ end
150
+ rescue ActiveRecord::RecordNotFound
151
+ error!('Request not found', 404)
152
+ rescue StandardError => e
153
+ error!("Error updating request: #{e.message}", 500)
154
+ end
155
+ end
156
+
157
+ namespace :outputs do
158
+ desc 'Delete one or multiple MTT outputs'
159
+ params do
160
+ requires :ids, type: Array[Integer], desc: 'Array of output IDs to delete'
161
+ end
162
+ delete do
163
+ # Find outputs where the parent request belongs to current user
164
+ outputs = Labimotion::DoseRespOutput
165
+ .joins(:dose_resp_request)
166
+ .where(
167
+ id: params[:ids],
168
+ dose_resp_requests: { created_by: current_user.id }
169
+ )
170
+
171
+ if outputs.empty?
172
+ error!('No outputs found or unauthorized', 404)
173
+ end
174
+
175
+ deleted_count = outputs.count
176
+ outputs.destroy_all
177
+
178
+ {
179
+ success: true,
180
+ message: "Successfully deleted #{deleted_count} output(s)",
181
+ deleted_count: deleted_count
182
+ }
183
+ rescue StandardError => e
184
+ error!("Error deleting outputs: #{e.message}", 500)
185
+ end
186
+ end
187
+
188
+ namespace :create_mtt_request do
189
+ desc 'Create MTT assay request'
190
+ params do
191
+ use :create_mtt_request_params
192
+ end
193
+ post do
194
+ # Find element and wellplates
195
+ element = Labimotion::Element.find_by(id: params[:id])
196
+ error!('Element not found', 404) unless element
197
+ #byebug
198
+ # Verify user has update permission
199
+ error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
200
+
201
+ wellplates = Wellplate.where(id: params[:wellplate_ids])
202
+ error!('No wellplates found', 404) if wellplates.empty?
203
+
204
+ # Generate wellplates metadata
205
+ wellplates_metadata = generate_wellplates_metadata(wellplates)
206
+
207
+ # Create DoseRespRequest record with token
208
+ dose_resp_request = Labimotion::DoseRespRequest.create!(
209
+ element_id: element.id,
210
+ wellplates_metadata: { wellplates: wellplates_metadata },
211
+ input_metadata: {
212
+ wellplate_ids: params[:wellplate_ids],
213
+ element_id: element.id,
214
+ element_name: element.name,
215
+ created_at: Time.current
216
+ },
217
+ state: Labimotion::DoseRespRequest::STATE_INITIAL,
218
+ created_by: current_user&.id,
219
+ expires_at: TPA_EXPIRATION.from_now
220
+ )
221
+
222
+ # Generate external app URL with token
223
+ token_uri = token_url(dose_resp_request)
224
+ external_app_url = get_external_app_url
225
+
226
+ # Format: "#{@app.url}?url=#{CGI.escape(token_uri)}&type=ThirdPartyApp"
227
+ "#{external_app_url}?method=DoseResponse&url=#{CGI.escape(token_uri)}"
228
+ rescue ActiveRecord::RecordInvalid => e
229
+ error!("Validation error: #{e.message}", 422)
230
+ rescue ActiveRecord::RecordNotFound => e
231
+ error!("Not found: #{e.message}", 404)
232
+ rescue StandardError => e
233
+ error!("Error: #{e.message}", 500)
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end