model_driven_api 3.5.4 → 3.5.5
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 +4 -4
- data/app/controllers/api/v2/info_controller.rb +576 -261
- data/lib/model_driven_api/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61a965405c27cbeed7e06d994c4cd97cd69ec81ba21e9eb1cd1f6feef3b32741
|
|
4
|
+
data.tar.gz: 414b957e13b3ccff5a2a3b999242ad3ae3a310f5c635f2edeab21264a2633cf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 269993f8be409f5f3fdea9509df7037cdbf6927d21efb28152c0a65e8e6c10dc549ed784490c39d0b758caa3140052e89499675aa3f55e391982d5887c5f97ad
|
|
7
|
+
data.tar.gz: d05fef4b0562fc228a3c5ff12ecb82f7a283fd5a5cbefe16a5482f8783fb9d03edac64689452f5b53c253b0432b02febcc233bf777faab4445fafae06863c1ee
|
|
@@ -3,7 +3,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
3
3
|
# Info uses a different auth method: username and password
|
|
4
4
|
skip_before_action :authenticate_request, only: [:version, :swagger, :openapi], raise: false
|
|
5
5
|
skip_before_action :extract_model, except: [:heartbeat]
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
# api :GET, '/api/v2/info/version', "Just prints the APPVERSION."
|
|
8
8
|
def version
|
|
9
9
|
render json: { version: "TODO: Find a Way to Dynamically Obtain It" }.to_json, status: 200
|
|
@@ -15,7 +15,6 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
15
15
|
render json: ::Role.all.to_json, status: 200
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
# api :GET, '/api/v2/info/heartbeat'
|
|
20
19
|
# Just keeps the session alive by returning a new token
|
|
21
20
|
def heartbeat
|
|
@@ -39,20 +38,20 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
39
38
|
if can? :read, d
|
|
40
39
|
model = d.to_s.underscore.tableize
|
|
41
40
|
pivot[model] ||= {}
|
|
42
|
-
d.columns_hash.each_pair do |key, val|
|
|
41
|
+
d.columns_hash.each_pair do |key, val|
|
|
43
42
|
pivot[model][key] = val.type unless key.ends_with? "_id"
|
|
44
43
|
end
|
|
45
44
|
# Only application record descendants to have a clean schema
|
|
46
45
|
pivot[model][:associations] ||= {
|
|
47
|
-
has_many: d.reflect_on_all_associations(:has_many).map { |a|
|
|
46
|
+
has_many: d.reflect_on_all_associations(:has_many).map { |a|
|
|
48
47
|
a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
|
|
49
48
|
}.compact,
|
|
50
49
|
has_one: d.reflect_on_all_associations(:has_one).map { |a|
|
|
51
50
|
a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
|
|
52
51
|
}.compact,
|
|
53
|
-
belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
|
|
52
|
+
belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
|
|
54
53
|
a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
|
|
55
|
-
}.compact
|
|
54
|
+
}.compact,
|
|
56
55
|
}
|
|
57
56
|
pivot[model][:methods] ||= (d.instance_methods(false).include?(:json_attrs) && !d.json_attrs.blank?) ? d.json_attrs[:methods] : nil
|
|
58
57
|
end
|
|
@@ -70,12 +69,12 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
70
69
|
method_class = instance.send(key).class.to_s
|
|
71
70
|
Rails.logger.debug "compute_type #{model} #{key} #{method_class}"
|
|
72
71
|
method_key = model.columns_hash[key]
|
|
73
|
-
|
|
72
|
+
|
|
74
73
|
# Not columns
|
|
75
74
|
return nil if method_key.nil?
|
|
76
75
|
return "object" if method_class == "ActiveStorage::Attached::One"
|
|
77
76
|
return "array" if method_class == "ActiveStorage::Attached::Many" || method_class == "Array" || method_class.ends_with?("Array") || method_class.ends_with?("Collection") || method_class.ends_with?("Relation") || method_class.ends_with?("Set") || method_class.ends_with?("List") || method_class.ends_with?("Queue") || method_class.ends_with?("Stack") || method_class.ends_with?("ActiveRecord_Associations_CollectionProxy")
|
|
78
|
-
|
|
77
|
+
|
|
79
78
|
# Columns
|
|
80
79
|
case method_key.type
|
|
81
80
|
when :json, :jsonb
|
|
@@ -106,9 +105,9 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
106
105
|
parsed_json = JSON.parse(model.new.to_json(dsl))
|
|
107
106
|
parsed_json.keys.map do |k|
|
|
108
107
|
type = compute_type(model, k)
|
|
109
|
-
|
|
108
|
+
|
|
110
109
|
# Remove fields that cannot be created or updated
|
|
111
|
-
if remove_reserved && %w( id created_at updated_at lock_version).include?(k.to_s)
|
|
110
|
+
if remove_reserved && %w( id created_at updated_at lock_version ).include?(k.to_s)
|
|
112
111
|
nil
|
|
113
112
|
elsif type == "method" && (parsed_json[k].is_a?(FalseClass) || parsed_json[k].is_a?(TrueClass))
|
|
114
113
|
[k, { "type": "boolean" }]
|
|
@@ -138,7 +137,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
138
137
|
end unless type.blank?
|
|
139
138
|
end.compact.to_h
|
|
140
139
|
end
|
|
141
|
-
|
|
140
|
+
|
|
142
141
|
def generate_paths
|
|
143
142
|
pivot = {
|
|
144
143
|
"/authenticate": {
|
|
@@ -147,7 +146,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
147
146
|
"tags": ["Authentication"],
|
|
148
147
|
"description": "Authenticate the user and return a JWT token in the header and the current user as body.",
|
|
149
148
|
"security": [
|
|
150
|
-
"basicAuth": []
|
|
149
|
+
"basicAuth": [],
|
|
151
150
|
],
|
|
152
151
|
"requestBody": {
|
|
153
152
|
"content": {
|
|
@@ -160,19 +159,19 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
160
159
|
"properties": {
|
|
161
160
|
"email": {
|
|
162
161
|
"type": "string",
|
|
163
|
-
"format": "email"
|
|
162
|
+
"format": "email",
|
|
164
163
|
},
|
|
165
164
|
"password": {
|
|
166
165
|
"type": "string",
|
|
167
|
-
"format": "password"
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
166
|
+
"format": "password",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
171
170
|
},
|
|
172
|
-
"required": ["email", "password"]
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
171
|
+
"required": ["email", "password"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
176
175
|
},
|
|
177
176
|
"responses": {
|
|
178
177
|
"200": {
|
|
@@ -181,25 +180,25 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
181
180
|
"token": {
|
|
182
181
|
"description": "JWT",
|
|
183
182
|
"schema": {
|
|
184
|
-
"type": "string"
|
|
185
|
-
}
|
|
186
|
-
}
|
|
183
|
+
"type": "string",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
187
186
|
},
|
|
188
187
|
"content": {
|
|
189
188
|
"application/json": {
|
|
190
189
|
"schema": {
|
|
191
190
|
"type": "object",
|
|
192
191
|
# ["id", "email", "created_at", "admin", "locked", "supplier_id", "location_id", "roles"]
|
|
193
|
-
"properties": create_properties_from_model(User, User.json_attrs)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
192
|
+
"properties": create_properties_from_model(User, User.json_attrs),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
197
196
|
},
|
|
198
197
|
"401": {
|
|
199
|
-
"description": "Unauthorized"
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
198
|
+
"description": "Unauthorized",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
203
202
|
},
|
|
204
203
|
"/raw/sql": {
|
|
205
204
|
"post": {
|
|
@@ -207,7 +206,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
207
206
|
"description": "Executes a SQL query on the underlying PostgreSQL database, the query must return the JSON in a **result** key (please note in the examples the _SELECT json_agg(u) AS result_ or in the more complex one the _SELECT jsonb_agg(pick_data) AS result_, they always use the **result** return object).\n \nDesigned for SELECT queries that use the json_agg function to aggregate results into JSON arrays, which must return the JSON in a **result** key.\n \nOther query types are not recommended and may be restricted for security and performance reasons.\n \nOnly SELECT statements are allowed. DDL and DML statements (INSERT, UPDATE, DELETE) are forbidden.\n \nQueries can be as simple as:\n \n```sql\nSELECT json_agg(u) AS result\nFROM users u\nWHERE u.active = true;\n```\n \nor more complex, using joins, subqueries, CTEs, and other SQL features. like:\n \n```sql\nWITH pick_data AS (\n SELECT p.id,\n p.project_id,\n p.quantity,\n p.created_at,\n p.updated_at,\n p.notes,\n p.document_id,\n p.external_code,\n p.reference_project_id,\n p.reference_row,\n p.closed,\n p.parent_reference_row,\n p.packages,\n p.weight,\n p.dispatched_quantity,\n p.override_item_reference,\n p.override_item_description,\n p.override_item_measure_unit,\n p.lock_version,\n p.user_id,\n COALESCE(SUM(pr.quantity), 0) AS quantity_detected,\n COALESCE(p.quantity, 0) - COALESCE(SUM(pr.quantity), 0) AS quantity_remaining,\n json_agg(\n jsonb_build_object(\n 'id',\n pr.id,\n 'item_id',\n pr.item_id,\n 'location_id',\n pr.location_id,\n 'quantity',\n pr.quantity\n )\n ) AS project_rows,\n jsonb_build_object(\n 'id',\n l.id,\n 'name',\n l.name,\n 'description',\n l.description\n ) AS location,\n jsonb_build_object(\n 'id',\n i.id,\n 'code',\n i.code,\n 'created_at',\n i.created_at,\n 'updated_at',\n i.updated_at,\n 'description',\n i.description,\n 'has_serials',\n i.has_serials,\n 'external_code',\n i.external_code,\n 'barcode',\n i.barcode,\n 'weight',\n i.weight,\n 'quantity',\n i.quantity,\n 'package_quantity',\n i.package_quantity,\n 'locked_quantity',\n i.locked_quantity,\n 'disabled',\n i.disabled,\n 'measure_unit',\n jsonb_build_object('id', mu.id, 'name', mu.name),\n 'location',\n jsonb_build_object('id', il.id, 'name', il.name),\n 'locations',\n (\n SELECT jsonb_agg(\n jsonb_build_object('id', loc.id, 'name', loc.name)\n )\n FROM locations loc\n JOIN item_locations il ON il.location_id = loc.id\n WHERE il.item_id = i.id\n ),\n 'additional_barcodes',\n (\n SELECT jsonb_agg(\n jsonb_build_object('id', ab.id, 'code', ab.code)\n )\n FROM additional_barcodes ab\n WHERE ab.item_id = i.id\n )\n ) AS item\n FROM picks p\n LEFT JOIN project_rows pr ON pr.pick_id = p.id\n LEFT JOIN locations l ON l.id = p.location_id\n LEFT JOIN items i ON i.id = p.item_id\n LEFT JOIN measure_units mu ON mu.id = i.measure_unit_id\n LEFT JOIN locations il ON il.id = i.location_id\n WHERE p.project_id = 16130\n GROUP BY p.id,\n l.id,\n i.id,\n mu.id,\n il.id\n)\nSELECT jsonb_agg(pick_data) AS result\nFROM pick_data;\n```\n \nLet's break down the provided SQL query and understand why it uses a Common Table Expression (CTE) and how it can improve performance.\n \n### Explanation of the complex Query\n \nThe provided query uses a CTE named `pick_data` to gather and aggregate data from multiple tables (`picks`, `project_rows`, `locations`, `items`, `measure_units`, `item_locations`, and `additional_barcodes`). The final result is a JSON array of aggregated data.\n \n#### Key Components of the Query:\n \n1. **CTE Definition**:\n \n ```sql\n WITH pick_data AS (\n -- Subquery content\n )\n ```\n \n The CTE `pick_data` is defined to encapsulate the logic of the subquery. This makes the query more readable and modular.\n \n2. **Data Selection and Aggregation**:\n Inside the CTE, data is selected and aggregated from various tables. Key operations include:\n \n - **Column Selection**: Selecting specific columns from the `picks` table.\n - **Aggregation**: Using `COALESCE` and `SUM` to calculate `quantity_detected` and `quantity_remaining`.\n - **JSON Aggregation**: Using `json_agg` and `jsonb_build_object` to create JSON objects and arrays for nested data structures.\n \n3. **Final Selection**:\n ```sql\n SELECT jsonb_agg(pick_data) AS result FROM pick_data;\n ```\n The final selection aggregates all rows from the CTE `pick_data` into a single JSON array.\n \n### Why Use a CTE?\n \n1. **Readability and Maintainability**:\n \n - **Modular Code**: By using a CTE, the complex logic is encapsulated in a named subquery, making the main query easier to read and understand.\n - **Reusability**: The CTE can be reused within the same query if needed, avoiding duplication of code.\n \n2. **Performance**:\n - **Optimization**: Modern SQL engines can optimize CTEs effectively. They can be materialized (computed once and stored) or inlined (expanded in the main query) based on the query plan.\n - **Intermediate Results**: CTEs allow breaking down complex queries into simpler steps, which can sometimes help the SQL engine optimize each step more effectively.\n \n### Documentation for Editing Generic Queries\n \nWhen editing or creating new queries, consider the following steps and best practices:\n \n1. **Identify the Purpose**:\n \n - Clearly define what the query needs to achieve. Understand the data relationships and the final output format.\n \n2. **Use CTEs for Complex Logic**:\n \n - Break down complex queries into smaller, manageable parts using CTEs. This improves readability and maintainability.\n \n3. **Optimize Aggregations and Joins**:\n \n - Ensure that aggregations and joins are optimized. Use indexes where appropriate and avoid unnecessary computations.\n \n4. **Leverage JSON Functions**:\n \n - Use JSON functions (`json_agg`, `jsonb_build_object`, etc.) to handle nested data structures effectively.\n \n5. **Test and Validate**:\n - Test the query with different datasets to ensure it performs well and returns the correct results. Validate the output format.\n \n### Example of a Generic Query Using CTE\n \nHere's a generic example to illustrate how to use a CTE in a query:\n \n```sql\n \n\nWITH data_aggregation AS (\n SELECT\n t1.id,\n t1.name,\n SUM(t2.value) AS total_value,\n json_agg(\n jsonb_build_object(\n 'id', t2.id,\n 'value', t2.value\n )\n ) AS details\n FROM table1 t1\n LEFT JOIN table2 t2 ON t2.table1_id = t1.id\n GROUP BY t1.id\n)\nSELECT jsonb_agg(data_aggregation) AS result FROM data_aggregation;\n```\n \n### Conclusion\n \nUsing CTEs in SQL queries helps in organizing complex logic, improving readability, and potentially enhancing performance. When editing or creating new queries, follow best practices such as breaking down complex logic, optimizing joins and aggregations, and leveraging JSON functions for nested data structures.\n",
|
|
208
207
|
"tags": ["Raw"],
|
|
209
208
|
"security": [
|
|
210
|
-
"bearerAuth": []
|
|
209
|
+
"bearerAuth": [],
|
|
211
210
|
],
|
|
212
211
|
"responses": {
|
|
213
212
|
"200": {
|
|
@@ -220,13 +219,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
220
219
|
"type": "object",
|
|
221
220
|
"properties": {
|
|
222
221
|
"json_agg": {
|
|
223
|
-
"type": "string"
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
222
|
+
"type": "string",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
230
229
|
},
|
|
231
230
|
"400": {
|
|
232
231
|
"description": "SQL query must return a key called result otherwise cannot be parsed",
|
|
@@ -236,13 +235,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
236
235
|
"type": "object",
|
|
237
236
|
"properties": {
|
|
238
237
|
"error": {
|
|
239
|
-
"type": "string"
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
238
|
+
"type": "string",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
246
245
|
|
|
247
246
|
},
|
|
248
247
|
"requestBody": {
|
|
@@ -253,14 +252,14 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
253
252
|
"properties": {
|
|
254
253
|
"query": {
|
|
255
254
|
"type": "string",
|
|
256
|
-
"example": "SELECT json_agg(u) FROM users u WHERE u.active = true;"
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
255
|
+
"example": "SELECT json_agg(u) FROM users u WHERE u.active = true;",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
264
263
|
},
|
|
265
264
|
"/info/version": {
|
|
266
265
|
"get": {
|
|
@@ -273,13 +272,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
273
272
|
"content": {
|
|
274
273
|
"application/json": {
|
|
275
274
|
"schema": {
|
|
276
|
-
"type": "string"
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
275
|
+
"type": "string",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
283
282
|
},
|
|
284
283
|
"/info/heartbeat": {
|
|
285
284
|
"get": {
|
|
@@ -287,7 +286,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
287
286
|
"description": "Just keeps the session alive by returning a new token",
|
|
288
287
|
"tags": ["Info"],
|
|
289
288
|
"security": [
|
|
290
|
-
"bearerAuth": []
|
|
289
|
+
"bearerAuth": [],
|
|
291
290
|
],
|
|
292
291
|
"responses": {
|
|
293
292
|
"200": {
|
|
@@ -296,13 +295,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
296
295
|
"token": {
|
|
297
296
|
"description": "JWT",
|
|
298
297
|
"schema": {
|
|
299
|
-
"type": "string"
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
298
|
+
"type": "string",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
306
305
|
},
|
|
307
306
|
"/info/roles": {
|
|
308
307
|
"get": {
|
|
@@ -310,7 +309,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
310
309
|
"description": "Returns the roles list",
|
|
311
310
|
"tags": ["Info"],
|
|
312
311
|
"security": [
|
|
313
|
-
"bearerAuth": []
|
|
312
|
+
"bearerAuth": [],
|
|
314
313
|
],
|
|
315
314
|
"responses": {
|
|
316
315
|
"200": {
|
|
@@ -323,22 +322,22 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
323
322
|
"type": "object",
|
|
324
323
|
"properties": {
|
|
325
324
|
"id": {
|
|
326
|
-
"type": "integer"
|
|
325
|
+
"type": "integer",
|
|
327
326
|
},
|
|
328
327
|
"name": {
|
|
329
|
-
"type": "string"
|
|
328
|
+
"type": "string",
|
|
330
329
|
},
|
|
331
330
|
"description": {
|
|
332
|
-
"type": "string"
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
331
|
+
"type": "string",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
342
341
|
},
|
|
343
342
|
"/info/schema": {
|
|
344
343
|
"get": {
|
|
@@ -346,7 +345,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
346
345
|
"description": "Returns the schema of the models",
|
|
347
346
|
"tags": ["Info"],
|
|
348
347
|
"security": [
|
|
349
|
-
"bearerAuth": []
|
|
348
|
+
"bearerAuth": [],
|
|
350
349
|
],
|
|
351
350
|
"responses": {
|
|
352
351
|
"200": {
|
|
@@ -359,24 +358,24 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
359
358
|
"type": "object",
|
|
360
359
|
"properties": {
|
|
361
360
|
"id": {
|
|
362
|
-
"type": "integer"
|
|
361
|
+
"type": "integer",
|
|
363
362
|
},
|
|
364
363
|
"created_at": {
|
|
365
364
|
"type": "string",
|
|
366
|
-
"format": "date-time"
|
|
365
|
+
"format": "date-time",
|
|
367
366
|
},
|
|
368
367
|
"updated_at": {
|
|
369
368
|
"type": "string",
|
|
370
|
-
"format": "date-time"
|
|
369
|
+
"format": "date-time",
|
|
371
370
|
},
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
380
379
|
},
|
|
381
380
|
"/info/dsl": {
|
|
382
381
|
"get": {
|
|
@@ -384,7 +383,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
384
383
|
"description": "Returns the DSL of the models",
|
|
385
384
|
"tags": ["Info"],
|
|
386
385
|
"security": [
|
|
387
|
-
"bearerAuth": []
|
|
386
|
+
"bearerAuth": [],
|
|
388
387
|
],
|
|
389
388
|
"responses": {
|
|
390
389
|
"200": {
|
|
@@ -395,23 +394,23 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
395
394
|
"type": "object",
|
|
396
395
|
"properties": {
|
|
397
396
|
"id": {
|
|
398
|
-
"type": "integer"
|
|
397
|
+
"type": "integer",
|
|
399
398
|
},
|
|
400
399
|
"created_at": {
|
|
401
400
|
"type": "string",
|
|
402
|
-
"format": "date-time"
|
|
401
|
+
"format": "date-time",
|
|
403
402
|
},
|
|
404
403
|
"updated_at": {
|
|
405
404
|
"type": "string",
|
|
406
|
-
"format": "date-time"
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
405
|
+
"format": "date-time",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
415
414
|
},
|
|
416
415
|
"/info/translations": {
|
|
417
416
|
"get": {
|
|
@@ -419,7 +418,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
419
418
|
"description": "Returns the translations of the entire App",
|
|
420
419
|
"tags": ["Info"],
|
|
421
420
|
"security": [
|
|
422
|
-
"bearerAuth": []
|
|
421
|
+
"bearerAuth": [],
|
|
423
422
|
],
|
|
424
423
|
"responses": {
|
|
425
424
|
"200": {
|
|
@@ -430,18 +429,18 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
430
429
|
"type": "object",
|
|
431
430
|
"properties": {
|
|
432
431
|
"key": {
|
|
433
|
-
"type": "string"
|
|
432
|
+
"type": "string",
|
|
434
433
|
},
|
|
435
434
|
"value": {
|
|
436
|
-
"type": "string"
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
435
|
+
"type": "string",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
445
444
|
},
|
|
446
445
|
"/info/settings": {
|
|
447
446
|
"get": {
|
|
@@ -449,7 +448,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
449
448
|
"description": "Returns the settings of the App",
|
|
450
449
|
"tags": ["Info"],
|
|
451
450
|
"security": [
|
|
452
|
-
"bearerAuth": []
|
|
451
|
+
"bearerAuth": [],
|
|
453
452
|
],
|
|
454
453
|
"responses": {
|
|
455
454
|
"200": {
|
|
@@ -463,20 +462,20 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
463
462
|
"type": "object",
|
|
464
463
|
"properties": {
|
|
465
464
|
"key": {
|
|
466
|
-
"type": "string"
|
|
465
|
+
"type": "string",
|
|
467
466
|
},
|
|
468
467
|
"value": {
|
|
469
|
-
"type": "string"
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
468
|
+
"type": "string",
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
480
479
|
},
|
|
481
480
|
"/info/swagger": {
|
|
482
481
|
"get": {
|
|
@@ -492,24 +491,24 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
492
491
|
"type": "object",
|
|
493
492
|
"properties": {
|
|
494
493
|
"id": {
|
|
495
|
-
"type": "integer"
|
|
494
|
+
"type": "integer",
|
|
496
495
|
},
|
|
497
496
|
"created_at": {
|
|
498
497
|
"type": "string",
|
|
499
|
-
"format": "date-time"
|
|
498
|
+
"format": "date-time",
|
|
500
499
|
},
|
|
501
500
|
"updated_at": {
|
|
502
501
|
"type": "string",
|
|
503
|
-
"format": "date-time"
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
502
|
+
"format": "date-time",
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
513
512
|
}
|
|
514
513
|
ApplicationRecord.subclasses.sort_by { |d| d.to_s }.each do |d|
|
|
515
514
|
# Only if current user can read the model
|
|
@@ -522,7 +521,7 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
522
521
|
"description": "Returns the list of #{model}",
|
|
523
522
|
"tags": [model.classify],
|
|
524
523
|
"security": [
|
|
525
|
-
"bearerAuth": []
|
|
524
|
+
"bearerAuth": [],
|
|
526
525
|
],
|
|
527
526
|
"responses": {
|
|
528
527
|
"200": {
|
|
@@ -533,23 +532,23 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
533
532
|
"type": "array",
|
|
534
533
|
"items": {
|
|
535
534
|
"type": "object",
|
|
536
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
535
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
541
540
|
},
|
|
542
541
|
"404": {
|
|
543
|
-
"description": "No #{model} found"
|
|
544
|
-
}
|
|
545
|
-
}
|
|
542
|
+
"description": "No #{model} found",
|
|
543
|
+
},
|
|
544
|
+
},
|
|
546
545
|
},
|
|
547
546
|
"post": {
|
|
548
547
|
"summary": "Create",
|
|
549
548
|
"description": "Creates a new #{model}",
|
|
550
549
|
"tags": [model.classify],
|
|
551
550
|
"security": [
|
|
552
|
-
"bearerAuth": []
|
|
551
|
+
"bearerAuth": [],
|
|
553
552
|
],
|
|
554
553
|
"requestBody": {
|
|
555
554
|
"content": {
|
|
@@ -559,12 +558,12 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
559
558
|
"properties": {
|
|
560
559
|
"#{model.singularize}": {
|
|
561
560
|
"type": "object",
|
|
562
|
-
"properties": create_properties_from_model(d, {}, true)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
561
|
+
"properties": create_properties_from_model(d, {}, true),
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
568
567
|
},
|
|
569
568
|
"responses": {
|
|
570
569
|
"200": {
|
|
@@ -573,23 +572,23 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
573
572
|
"application/json": {
|
|
574
573
|
"schema": {
|
|
575
574
|
"type": "object",
|
|
576
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
575
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
583
582
|
}
|
|
584
583
|
# Non CRUD or Search, but custom, usually bulk operations endpoints
|
|
585
584
|
new_custom_actions = ("Endpoints::#{d.model_name.name}".constantize.instance_methods(false) rescue [])
|
|
586
585
|
Rails.logger.debug "New Custom Actions (#{d.model_name.name}): #{new_custom_actions}"
|
|
587
586
|
new_custom_actions.each do |action|
|
|
588
587
|
openapi_definition = "Endpoints::#{d.model_name.name}".constantize.definitions[d.model_name.name][action.to_sym] rescue []
|
|
589
|
-
|
|
588
|
+
|
|
590
589
|
# Add the tag to the openapi definition
|
|
591
590
|
openapi_definition.each do |k, v|
|
|
592
|
-
v[:tags] = [
|
|
591
|
+
v[:tags] = [d.model_name.name]
|
|
593
592
|
end
|
|
594
593
|
|
|
595
594
|
pivot["/#{model}/custom_action/#{action}"] = openapi_definition if openapi_definition
|
|
@@ -598,10 +597,10 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
598
597
|
# Complex queries are made using ranskac search via a post endpoint
|
|
599
598
|
"post": {
|
|
600
599
|
"summary": "Search",
|
|
601
|
-
"description": "Searches the #{model} using complex queries. Please refer to the [documentation](https://activerecord-hackery.github.io/ransack/) for the query syntax
|
|
600
|
+
"description": "Searches the #{model} using complex queries. Please refer to the [documentation](https://activerecord-hackery.github.io/ransack/) for the query syntax and to the general description of this swagger document to discover the usage of other, non ransack predicates for example to count records, select only some fields and more.",
|
|
602
601
|
"tags": [model.classify],
|
|
603
602
|
"security": [
|
|
604
|
-
"bearerAuth": []
|
|
603
|
+
"bearerAuth": [],
|
|
605
604
|
],
|
|
606
605
|
"requestBody": {
|
|
607
606
|
"content": {
|
|
@@ -613,17 +612,17 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
613
612
|
"type": "object",
|
|
614
613
|
"properties": {
|
|
615
614
|
"name_or_description_cont": {
|
|
616
|
-
"type": "string"
|
|
615
|
+
"type": "string",
|
|
617
616
|
},
|
|
618
617
|
"first_name_eq": {
|
|
619
|
-
"type": "string"
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
618
|
+
"type": "string",
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
627
626
|
},
|
|
628
627
|
"responses": {
|
|
629
628
|
"200": {
|
|
@@ -634,17 +633,17 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
634
633
|
"type": "array",
|
|
635
634
|
"items": {
|
|
636
635
|
"type": "object",
|
|
637
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
636
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
642
641
|
},
|
|
643
642
|
"404": {
|
|
644
|
-
"description": "No #{model} found"
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
643
|
+
"description": "No #{model} found",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
},
|
|
648
647
|
}
|
|
649
648
|
pivot["/#{model}/{id}"] = {
|
|
650
649
|
"put": {
|
|
@@ -656,13 +655,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
656
655
|
"in": "path",
|
|
657
656
|
"required": true,
|
|
658
657
|
"schema": {
|
|
659
|
-
"type": "integer"
|
|
660
|
-
}
|
|
661
|
-
}
|
|
658
|
+
"type": "integer",
|
|
659
|
+
},
|
|
660
|
+
},
|
|
662
661
|
],
|
|
663
662
|
"tags": [model.classify],
|
|
664
663
|
"security": [
|
|
665
|
-
"bearerAuth": []
|
|
664
|
+
"bearerAuth": [],
|
|
666
665
|
],
|
|
667
666
|
"requestBody": {
|
|
668
667
|
"content": {
|
|
@@ -672,12 +671,12 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
672
671
|
"properties": {
|
|
673
672
|
"#{model.singularize}": {
|
|
674
673
|
"type": "object",
|
|
675
|
-
"properties": create_properties_from_model(d, {}, true)
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
674
|
+
"properties": create_properties_from_model(d, {}, true),
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
681
680
|
},
|
|
682
681
|
"responses": {
|
|
683
682
|
"200": {
|
|
@@ -686,15 +685,15 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
686
685
|
"application/json": {
|
|
687
686
|
"schema": {
|
|
688
687
|
"type": "object",
|
|
689
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
688
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
693
692
|
},
|
|
694
693
|
"404": {
|
|
695
|
-
"description": "No #{model} found"
|
|
696
|
-
}
|
|
697
|
-
}
|
|
694
|
+
"description": "No #{model} found",
|
|
695
|
+
},
|
|
696
|
+
},
|
|
698
697
|
},
|
|
699
698
|
"patch": {
|
|
700
699
|
"summary": "Patch",
|
|
@@ -705,13 +704,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
705
704
|
"in": "path",
|
|
706
705
|
"required": true,
|
|
707
706
|
"schema": {
|
|
708
|
-
"type": "integer"
|
|
709
|
-
}
|
|
710
|
-
}
|
|
707
|
+
"type": "integer",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
711
710
|
],
|
|
712
711
|
"tags": [model.classify],
|
|
713
712
|
"security": [
|
|
714
|
-
"bearerAuth": []
|
|
713
|
+
"bearerAuth": [],
|
|
715
714
|
],
|
|
716
715
|
"requestBody": {
|
|
717
716
|
"content": {
|
|
@@ -721,12 +720,12 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
721
720
|
"properties": {
|
|
722
721
|
"#{model.singularize}": {
|
|
723
722
|
"type": "object",
|
|
724
|
-
"properties": create_properties_from_model(d, {}, true)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
723
|
+
"properties": create_properties_from_model(d, {}, true),
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
},
|
|
730
729
|
},
|
|
731
730
|
"responses": {
|
|
732
731
|
"200": {
|
|
@@ -735,15 +734,15 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
735
734
|
"application/json": {
|
|
736
735
|
"schema": {
|
|
737
736
|
"type": "object",
|
|
738
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
737
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
742
741
|
},
|
|
743
742
|
"404": {
|
|
744
|
-
"description": "No #{model} found"
|
|
745
|
-
}
|
|
746
|
-
}
|
|
743
|
+
"description": "No #{model} found",
|
|
744
|
+
},
|
|
745
|
+
},
|
|
747
746
|
},
|
|
748
747
|
"delete": {
|
|
749
748
|
"summary": "Delete",
|
|
@@ -754,22 +753,22 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
754
753
|
"in": "path",
|
|
755
754
|
"required": true,
|
|
756
755
|
"schema": {
|
|
757
|
-
"type": "integer"
|
|
758
|
-
}
|
|
759
|
-
}
|
|
756
|
+
"type": "integer",
|
|
757
|
+
},
|
|
758
|
+
},
|
|
760
759
|
],
|
|
761
760
|
"tags": [model.classify],
|
|
762
761
|
"security": [
|
|
763
|
-
"bearerAuth": []
|
|
762
|
+
"bearerAuth": [],
|
|
764
763
|
],
|
|
765
764
|
"responses": {
|
|
766
765
|
"200": {
|
|
767
|
-
"description": "#{model} Deleted"
|
|
766
|
+
"description": "#{model} Deleted",
|
|
768
767
|
},
|
|
769
768
|
"404": {
|
|
770
|
-
"description": "No #{model} found"
|
|
771
|
-
}
|
|
772
|
-
}
|
|
769
|
+
"description": "No #{model} found",
|
|
770
|
+
},
|
|
771
|
+
},
|
|
773
772
|
},
|
|
774
773
|
"get": {
|
|
775
774
|
"summary": "Show",
|
|
@@ -780,13 +779,13 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
780
779
|
"in": "path",
|
|
781
780
|
"required": true,
|
|
782
781
|
"schema": {
|
|
783
|
-
"type": "integer"
|
|
784
|
-
}
|
|
785
|
-
}
|
|
782
|
+
"type": "integer",
|
|
783
|
+
},
|
|
784
|
+
},
|
|
786
785
|
],
|
|
787
786
|
"tags": [model.classify],
|
|
788
787
|
"security": [
|
|
789
|
-
"bearerAuth": []
|
|
788
|
+
"bearerAuth": [],
|
|
790
789
|
],
|
|
791
790
|
"responses": {
|
|
792
791
|
"200": {
|
|
@@ -795,26 +794,26 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
795
794
|
"application/json": {
|
|
796
795
|
"schema": {
|
|
797
796
|
"type": "object",
|
|
798
|
-
"properties": create_properties_from_model(d, (d.json_attrs rescue {}))
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
797
|
+
"properties": create_properties_from_model(d, (d.json_attrs rescue {})),
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
802
801
|
},
|
|
803
802
|
"404": {
|
|
804
|
-
"description": "No #{model} found"
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
803
|
+
"description": "No #{model} found",
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
},
|
|
808
807
|
}
|
|
809
|
-
# d.columns_hash.each_pair do |key, val|
|
|
808
|
+
# d.columns_hash.each_pair do |key, val|
|
|
810
809
|
# pivot[model][key] = val.type unless key.ends_with? "_id"
|
|
811
810
|
# end
|
|
812
811
|
# # Only application record descendants in order to have a clean schema
|
|
813
812
|
# pivot[model][:associations] ||= {
|
|
814
|
-
# has_many: d.reflect_on_all_associations(:has_many).map { |a|
|
|
813
|
+
# has_many: d.reflect_on_all_associations(:has_many).map { |a|
|
|
815
814
|
# a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
|
|
816
|
-
# }.compact,
|
|
817
|
-
# belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
|
|
815
|
+
# }.compact,
|
|
816
|
+
# belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
|
|
818
817
|
# a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
|
|
819
818
|
# }.compact
|
|
820
819
|
# }
|
|
@@ -824,6 +823,323 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
824
823
|
pivot
|
|
825
824
|
end
|
|
826
825
|
|
|
826
|
+
def info_description
|
|
827
|
+
info = <<-MARKDOWN
|
|
828
|
+
|
|
829
|
+
## About this API Documentation
|
|
830
|
+
|
|
831
|
+
Model Driven Backend [API](https://github.com/gabrieletassoni/thecore/blob/master/docs/04_REST_API.md) created to reflect the actual Active Record Models present in the project in a dynamic way.
|
|
832
|
+
|
|
833
|
+
This swagger describes all the CRUD endpoints provided by the application, as well as all the custom endpoints and gives a deep dive into the parameters accepted in GET (Index) requests and POST (Search) requests. Since the controller unifies params (via request.parameters), the filtering logic is identical whether parameters are passed in the Query String (GET) or the JSON Body (POST).
|
|
834
|
+
|
|
835
|
+
The documentation starts from the authentication mechanism, integrated with the details from the provided code.
|
|
836
|
+
Here is the updated and integrated documentation for the Authentication mechanism. It now covers the full lifecycle: obtaining the initial token via the login endpoint and maintaining the session via the sliding expiration mechanism found in the controller.
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Authentication & Token Management
|
|
841
|
+
|
|
842
|
+
The API implements a **stateless JWT (JSON Web Token)** authentication mechanism. It consists of two distinct phases:
|
|
843
|
+
|
|
844
|
+
1. **Initial Authentication:** Exchanging credentials for the first Token.
|
|
845
|
+
2. **Session Maintenance:** Using a **Sliding Expiration** strategy where every subsequent successful request issues a fresh token.
|
|
846
|
+
|
|
847
|
+
### 1. Initial Authentication (Login)
|
|
848
|
+
|
|
849
|
+
To begin a session, the client must POST user credentials to the authentication endpoint. This is the only request that does not require an `Authorization` header.
|
|
850
|
+
|
|
851
|
+
#### Request
|
|
852
|
+
|
|
853
|
+
**Endpoint:** `POST /api/v2/authenticate`
|
|
854
|
+
|
|
855
|
+
**Body:**
|
|
856
|
+
|
|
857
|
+
```json
|
|
858
|
+
{
|
|
859
|
+
"auth": {
|
|
860
|
+
"email": "admin@example.com",
|
|
861
|
+
"password": "Change#1"
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
#### Response
|
|
868
|
+
|
|
869
|
+
Upon successful authentication, the server returns two critical pieces of data:
|
|
870
|
+
|
|
871
|
+
1. **Response Body:** Contains the User object details.
|
|
872
|
+
2. **Response Headers:** Contains the initial JWT in the `token` header.
|
|
873
|
+
|
|
874
|
+
**Example Body:**
|
|
875
|
+
|
|
876
|
+
```json
|
|
877
|
+
{
|
|
878
|
+
"id": 219,
|
|
879
|
+
"email": "admin@example.com",
|
|
880
|
+
"created_at": "2025-12-10T07:57:54.336Z",
|
|
881
|
+
"admin": true,
|
|
882
|
+
"locked": false,
|
|
883
|
+
"locale": "en",
|
|
884
|
+
// ... other user attributes
|
|
885
|
+
"roles": []
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**Example Headers:**
|
|
891
|
+
Note the presence of the `token` header.
|
|
892
|
+
|
|
893
|
+
```http
|
|
894
|
+
HTTP/1.1 200 OK
|
|
895
|
+
content-type: application/json; charset=utf-8
|
|
896
|
+
token: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyMTksImV4cCI6MTc2NjQ3ODMwN30.I0qJzOwA0Jxx0frL5-9jVH2PsakdZjSEY8Kqb9S3GKo
|
|
897
|
+
x-request-id: 113cad63-11f8-4daf-b684-19322a053bcc
|
|
898
|
+
...
|
|
899
|
+
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
### 2. Sliding Expiration (Token Renewal)
|
|
905
|
+
|
|
906
|
+
Once the client has the initial token, the `Api::V2::ApplicationController` handles the lifecycle. Instead of a fixed expiration time that forces a re-login, the API issues a **brand new token** with every successfully authenticated request.
|
|
907
|
+
|
|
908
|
+
#### The Renewal Mechanism
|
|
909
|
+
|
|
910
|
+
1. **Client Request:** The client sends the *current* token in the `Authorization` header.
|
|
911
|
+
```http
|
|
912
|
+
Authorization: Bearer <Current_Token>
|
|
913
|
+
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
2. **Verification:** The `authenticate_request` method verifies the token. If valid, it sets `@current_user`.
|
|
918
|
+
3. **Renewal:** Before sending the response, the controller generates a new JWT encoded with the current user's ID and sets it in the response header.
|
|
919
|
+
```ruby
|
|
920
|
+
response.set_header("Token", JsonWebToken.encode(user_id: current_user.id))
|
|
921
|
+
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### Client-Side Implementation Guide
|
|
925
|
+
|
|
926
|
+
To maintain a valid session, the client must implement an interceptor to handle token rotation:
|
|
927
|
+
|
|
928
|
+
1. **Login:** Call `/api/v2/authenticate` and store the `token` from the response header.
|
|
929
|
+
2. **Subsequent Requests:** Attach the stored token to the `Authorization: Bearer ...` header.
|
|
930
|
+
3. **Update Storage:**
|
|
931
|
+
* Check every response for a `Token` header.
|
|
932
|
+
* **If present:** Immediately replace the stored token with this new value.
|
|
933
|
+
* **If missing:** Continue using the existing token (unless the response was a 401/403 error).
|
|
934
|
+
|
|
935
|
+
#### Failure Scenarios
|
|
936
|
+
|
|
937
|
+
If the `authenticate_request` fails (e.g., token expired, invalid signature):
|
|
938
|
+
|
|
939
|
+
* The controller returns an unauthenticated error (`unauthenticated!`).
|
|
940
|
+
* The execution halts, and the line generating the new header is never reached.
|
|
941
|
+
* **Result:** The client receives a 401 error and **no new token**, signaling that the user must perform the **Initial Authentication** (login) again.
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## API Documentation: Search, Filtering, and Pagination Parameters
|
|
946
|
+
|
|
947
|
+
### 1. Pagination and Counting
|
|
948
|
+
|
|
949
|
+
These parameters control the amount of data returned and navigation through result pages.
|
|
950
|
+
|
|
951
|
+
* **`page`** (Integer): Indicates the page number to retrieve. If omitted, pagination is not applied (or defaults to the model's Kaminari configuration).
|
|
952
|
+
* **`per`** (Integer): Indicates the number of records per page. Works in conjunction with `page`.
|
|
953
|
+
* **`count`** (Boolean/Any value): If present and not empty, the API **does not** return the list of records but a JSON object containing only the total count of records matching the search criteria (e.g., `{ "count": 42 }`). Useful for displaying total results before loading them.
|
|
954
|
+
|
|
955
|
+
### 2. Field Selection
|
|
956
|
+
|
|
957
|
+
It is possible to limit the fields returned in the JSON response or include associations. The controller looks for these parameters in `a` or `json_attrs`.
|
|
958
|
+
|
|
959
|
+
* **`a`** (or `json_attrs`): An object or hash defining the output JSON structure.
|
|
960
|
+
* **`only`**: Array of strings. Returns only the specified attributes of the main model.
|
|
961
|
+
* **`methods`**: Array of strings. Includes model methods that are not database columns.
|
|
962
|
+
* **`include`**: Object for including relationships (associations). Associations can also have their own `only` or `methods`.
|
|
963
|
+
|
|
964
|
+
### 3. Custom Actions
|
|
965
|
+
|
|
966
|
+
* **`do`**: Specifies a custom action (`custom_action`) to execute on the model instead of the standard `index`.
|
|
967
|
+
* Format: `?do=action_name` or `?do=action_name-token`.
|
|
968
|
+
* The controller will look for a class method `custom_action_action_name` or a module `Endpoints::Model`.
|
|
969
|
+
|
|
970
|
+
### 4. Filters and Sorting (Ransack)
|
|
971
|
+
|
|
972
|
+
The core of the search functionality lies in the **`q`** parameter. The controller implements the **Ransack** gem, allowing for complex queries, filtering on associations, and dynamic sorting.
|
|
973
|
+
|
|
974
|
+
Basic structure: `q[field_name_predicate]=value`
|
|
975
|
+
|
|
976
|
+
#### Common Predicates (Suffixes)
|
|
977
|
+
|
|
978
|
+
* `_eq`: Equal to (e.g., `status_eq`).
|
|
979
|
+
* `_cont`: Contains (LIKE %value%, case insensitive).
|
|
980
|
+
* `_start`: Starts with.
|
|
981
|
+
* `_end`: Ends with.
|
|
982
|
+
* `_gt` / `_lt`: Greater than / Less than (for numbers or dates).
|
|
983
|
+
* `_gteq` / `_lteq`: Greater than or equal to / Less than or equal to.
|
|
984
|
+
* `_in`: Included in a list (accepts an array).
|
|
985
|
+
* `_present`: If set to `1` or `true`, filters for non-null values. `_blank` for nulls.
|
|
986
|
+
|
|
987
|
+
#### Sorting
|
|
988
|
+
|
|
989
|
+
* `s`: Defines the sorting order. Format: `field_name asc` or `field_name desc`.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## Practical Examples
|
|
994
|
+
|
|
995
|
+
Below are two usage scenarios to achieve the same result: a **GET** request (URL parameters) and a **POST** request (JSON Body parameters).
|
|
996
|
+
|
|
997
|
+
#### Scenario A: Simple Search and Pagination
|
|
998
|
+
|
|
999
|
+
**Goal:** Find users whose name contains "Mario", paginated (page 2, 10 per page).
|
|
1000
|
+
|
|
1001
|
+
##### 1. Using GET (Query String)
|
|
1002
|
+
|
|
1003
|
+
Parameters are "flattened" and encoded in the URL.
|
|
1004
|
+
|
|
1005
|
+
```text
|
|
1006
|
+
GET /api/v2/users?q[name_cont]=Mario&page=2&per=10
|
|
1007
|
+
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
##### 2. Using POST (JSON Body)
|
|
1011
|
+
|
|
1012
|
+
Ideal for complex searches to avoid exceeding URL length limits.
|
|
1013
|
+
|
|
1014
|
+
```json
|
|
1015
|
+
POST /api/v2/users/search
|
|
1016
|
+
Content-Type: application/json
|
|
1017
|
+
|
|
1018
|
+
{
|
|
1019
|
+
"q": {
|
|
1020
|
+
"name_cont": "Mario"
|
|
1021
|
+
},
|
|
1022
|
+
"page": 2,
|
|
1023
|
+
"per": 10
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
#### Scenario B: Advanced Search, Sorting, and Field Selection
|
|
1031
|
+
|
|
1032
|
+
**Goal:**
|
|
1033
|
+
|
|
1034
|
+
1. Search for orders (`orders`) where `total_price` is greater than 50.
|
|
1035
|
+
2. Belonging to a user (`user`) whose email ends with `@test.com`.
|
|
1036
|
+
3. Sort by creation date descending.
|
|
1037
|
+
4. Return only the `id` and `total_price` of the order, including the `email` of the associated user.
|
|
1038
|
+
|
|
1039
|
+
##### 1. Using GET (Query String)
|
|
1040
|
+
|
|
1041
|
+
Note the square bracket syntax for nested structures (`q`, `a`).
|
|
1042
|
+
|
|
1043
|
+
```text
|
|
1044
|
+
GET /api/v2/orders?q[total_price_gt]=50&q[user_email_end]=@test.com&q[s]=created_at desc&a[only][]=id&a[only][]=total_price&a[include][user][only][]=email
|
|
1045
|
+
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
##### 2. Using POST (JSON Body)
|
|
1049
|
+
|
|
1050
|
+
Much more readable for nested structures like `a` (json_attrs).
|
|
1051
|
+
|
|
1052
|
+
```json
|
|
1053
|
+
POST /api/v2/orders/search
|
|
1054
|
+
Content-Type: application/json
|
|
1055
|
+
|
|
1056
|
+
{
|
|
1057
|
+
"q": {
|
|
1058
|
+
"total_price_gt": 50,
|
|
1059
|
+
"user_email_end": "@test.com",
|
|
1060
|
+
"s": "created_at desc"
|
|
1061
|
+
},
|
|
1062
|
+
"a": {
|
|
1063
|
+
"only": ["id", "total_price"],
|
|
1064
|
+
"include": {
|
|
1065
|
+
"user": {
|
|
1066
|
+
"only": ["email"]
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
#### Scenario C: Multiple Search (OR) and Arrays
|
|
1077
|
+
|
|
1078
|
+
**Goal:** Find products where status is "new" **OR** "refurbished" (using `_in`).
|
|
1079
|
+
|
|
1080
|
+
##### 1. Using GET (Query String)
|
|
1081
|
+
|
|
1082
|
+
To pass an array in GET, repeat the empty square brackets `[]`.
|
|
1083
|
+
|
|
1084
|
+
```text
|
|
1085
|
+
GET /api/v2/products?q[status_in][]=new&q[status_in][]=refurbished
|
|
1086
|
+
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
##### 2. Using POST (JSON Body)
|
|
1090
|
+
|
|
1091
|
+
```json
|
|
1092
|
+
POST /api/v2/products/search
|
|
1093
|
+
Content-Type: application/json
|
|
1094
|
+
|
|
1095
|
+
{
|
|
1096
|
+
"q": {
|
|
1097
|
+
"status_in": ["new", "refurbished"]
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
#### Scenario D: Count Only
|
|
1106
|
+
|
|
1107
|
+
**Goal:** Know how many users are active without downloading the data.
|
|
1108
|
+
|
|
1109
|
+
##### 1. Using GET
|
|
1110
|
+
|
|
1111
|
+
```text
|
|
1112
|
+
GET /api/v2/users?q[active_eq]=true&count=true
|
|
1113
|
+
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
##### 2. Using POST
|
|
1117
|
+
|
|
1118
|
+
```json
|
|
1119
|
+
POST /api/v2/users/search
|
|
1120
|
+
Content-Type: application/json
|
|
1121
|
+
|
|
1122
|
+
{
|
|
1123
|
+
"q": {
|
|
1124
|
+
"active_eq": true
|
|
1125
|
+
},
|
|
1126
|
+
"count": true
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
**Expected Response:**
|
|
1132
|
+
|
|
1133
|
+
```json
|
|
1134
|
+
{
|
|
1135
|
+
"count": 156
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
```
|
|
1139
|
+
MARKDOWN
|
|
1140
|
+
info
|
|
1141
|
+
end
|
|
1142
|
+
|
|
827
1143
|
# GET '/api/v2/info/schema'
|
|
828
1144
|
def openapi
|
|
829
1145
|
uri = URI(request.url)
|
|
@@ -831,39 +1147,39 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
831
1147
|
"openapi": "3.0.0",
|
|
832
1148
|
"info": {
|
|
833
1149
|
"title": "#{Settings.ns(:main).app_name} API",
|
|
834
|
-
"description":
|
|
835
|
-
"version": "v2"
|
|
1150
|
+
"description": info_description,
|
|
1151
|
+
"version": "v2",
|
|
836
1152
|
},
|
|
837
1153
|
"servers": [
|
|
838
1154
|
{
|
|
839
1155
|
# i.e. "http://localhost:3001/api/v2"
|
|
840
1156
|
"url": "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port.present?}/api/v2",
|
|
841
|
-
"description": "The URL at which this API responds."
|
|
842
|
-
}
|
|
1157
|
+
"description": "The URL at which this API responds.",
|
|
1158
|
+
},
|
|
843
1159
|
],
|
|
844
1160
|
# 1) Define the security scheme type (HTTP bearer)
|
|
845
|
-
"components":{
|
|
1161
|
+
"components": {
|
|
846
1162
|
"securitySchemes": {
|
|
847
1163
|
"basicAuth": {
|
|
848
1164
|
"type": "http",
|
|
849
|
-
"scheme": "basic"
|
|
1165
|
+
"scheme": "basic",
|
|
850
1166
|
},
|
|
851
1167
|
"bearerAuth": { # arbitrary name for the security scheme
|
|
852
1168
|
"type": "http",
|
|
853
1169
|
"scheme": "bearer",
|
|
854
|
-
"bearerFormat": "JWT" # optional, arbitrary value for documentation purposes
|
|
855
|
-
}
|
|
856
|
-
}
|
|
1170
|
+
"bearerFormat": "JWT", # optional, arbitrary value for documentation purposes
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
857
1173
|
},
|
|
858
1174
|
# 2) Apply the security globally to all operations
|
|
859
1175
|
"security": [
|
|
860
1176
|
{
|
|
861
|
-
"bearerAuth": [] # use the same name as above
|
|
862
|
-
}
|
|
1177
|
+
"bearerAuth": [], # use the same name as above
|
|
1178
|
+
},
|
|
863
1179
|
],
|
|
864
|
-
"paths": generate_paths
|
|
1180
|
+
"paths": generate_paths,
|
|
865
1181
|
}
|
|
866
|
-
|
|
1182
|
+
|
|
867
1183
|
render json: pivot.to_json, status: 200
|
|
868
1184
|
end
|
|
869
1185
|
|
|
@@ -883,7 +1199,6 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
|
883
1199
|
end
|
|
884
1200
|
|
|
885
1201
|
def settings
|
|
886
|
-
render json: ThecoreSettings::Setting.pluck(:ns, :key, :raw).inject({}){|result, array| (result[array.first] ||= {})[array.second] = array.third; result }.to_json, status: 200
|
|
1202
|
+
render json: ThecoreSettings::Setting.pluck(:ns, :key, :raw).inject({}) { |result, array| (result[array.first] ||= {})[array.second] = array.third; result }.to_json, status: 200
|
|
887
1203
|
end
|
|
888
|
-
|
|
889
1204
|
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.5.
|
|
4
|
+
version: 3.5.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriele Tassoni
|
|
@@ -169,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
169
169
|
- !ruby/object:Gem::Version
|
|
170
170
|
version: '0'
|
|
171
171
|
requirements: []
|
|
172
|
-
rubygems_version: 3.6.
|
|
172
|
+
rubygems_version: 3.6.9
|
|
173
173
|
specification_version: 4
|
|
174
174
|
summary: Convention based RoR engine which uses DB schema introspection to create
|
|
175
175
|
REST APIs.
|