model_driven_api 3.6.3 → 3.6.4

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.
@@ -0,0 +1,1238 @@
1
+ module Api
2
+ module OpenApi
3
+ class V2 < Base
4
+ def generate
5
+ pivot = {
6
+ "/authenticate": {
7
+ "post": {
8
+ "summary": "Authenticate",
9
+ "tags": ["Authentication"],
10
+ "description": "Authenticate the user and return a JWT token in the header and the current user as body.",
11
+ "security": [
12
+ "basicAuth": [],
13
+ ],
14
+ "requestBody": {
15
+ "content": {
16
+ "application/json": {
17
+ "schema": {
18
+ "type": "object",
19
+ "properties": {
20
+ "auth": {
21
+ "type": "object",
22
+ "properties": {
23
+ "email": {
24
+ "type": "string",
25
+ "format": "email",
26
+ },
27
+ "password": {
28
+ "type": "string",
29
+ "format": "password",
30
+ },
31
+ },
32
+ },
33
+ },
34
+ "required": ["email", "password"],
35
+ },
36
+ },
37
+ },
38
+ },
39
+ "responses": {
40
+ "200": {
41
+ "description": "User authenticated",
42
+ "headers": {
43
+ "token": {
44
+ "description": "JWT",
45
+ "schema": {
46
+ "type": "string",
47
+ },
48
+ },
49
+ },
50
+ "content": {
51
+ "application/json": {
52
+ "schema": {
53
+ "type": "object",
54
+ # ["id", "email", "created_at", "admin", "locked", "supplier_id", "location_id", "roles"]
55
+ "properties": create_properties_from_model(User, User.json_attrs),
56
+ },
57
+ },
58
+ },
59
+ },
60
+ "401": {
61
+ "description": "Unauthorized",
62
+ },
63
+ },
64
+ },
65
+ },
66
+ "/raw/sql": {
67
+ "post": {
68
+ "summary": "Raw SQL query execution of SELECT queries",
69
+ "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",
70
+ "tags": ["Raw"],
71
+ "security": [
72
+ "bearerAuth": [],
73
+ ],
74
+ "responses": {
75
+ "200": {
76
+ "description": "SQL Query Result",
77
+ "content": {
78
+ "application/json": {
79
+ "schema": {
80
+ "type": "array",
81
+ "items": {
82
+ "type": "object",
83
+ "properties": {
84
+ "json_agg": {
85
+ "type": "string",
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ "400": {
94
+ "description": "SQL query must return a key called result otherwise cannot be parsed",
95
+ "content": {
96
+ "application/json": {
97
+ "schema": {
98
+ "type": "object",
99
+ "properties": {
100
+ "error": {
101
+ "type": "string",
102
+ },
103
+ },
104
+ },
105
+ },
106
+ },
107
+ },
108
+
109
+ },
110
+ "requestBody": {
111
+ "content": {
112
+ "application/json": {
113
+ "schema": {
114
+ "type": "object",
115
+ "properties": {
116
+ "query": {
117
+ "type": "string",
118
+ "example": "SELECT json_agg(u) FROM users u WHERE u.active = true;",
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ "/info/version": {
128
+ "get": {
129
+ "summary": "Version",
130
+ "description": "Just prints the APPVERSION",
131
+ "tags": ["Info"],
132
+ "responses": {
133
+ "200": {
134
+ "description": "APPVERSION",
135
+ "content": {
136
+ "application/json": {
137
+ "schema": {
138
+ "type": "string",
139
+ },
140
+ },
141
+ },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ "/info/heartbeat": {
147
+ "get": {
148
+ "summary": "Heartbeat",
149
+ "description": "Just keeps the session alive by returning a new token",
150
+ "tags": ["Info"],
151
+ "security": [
152
+ "bearerAuth": [],
153
+ ],
154
+ "responses": {
155
+ "200": {
156
+ "description": "Session alive",
157
+ "headers": {
158
+ "token": {
159
+ "description": "JWT",
160
+ "schema": {
161
+ "type": "string",
162
+ },
163
+ },
164
+ },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ "/info/roles": {
170
+ "get": {
171
+ "summary": "Roles",
172
+ "description": "Returns the roles list",
173
+ "tags": ["Info"],
174
+ "security": [
175
+ "bearerAuth": [],
176
+ ],
177
+ "responses": {
178
+ "200": {
179
+ "description": "Roles list",
180
+ "content": {
181
+ "application/json": {
182
+ "schema": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "object",
186
+ "properties": {
187
+ "id": {
188
+ "type": "integer",
189
+ },
190
+ "name": {
191
+ "type": "string",
192
+ },
193
+ "description": {
194
+ "type": "string",
195
+ },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ },
204
+ },
205
+ "/info/schema": {
206
+ "get": {
207
+ "summary": "Schema",
208
+ "description": "Returns the schema of the models",
209
+ "tags": ["Info"],
210
+ "security": [
211
+ "bearerAuth": [],
212
+ ],
213
+ "responses": {
214
+ "200": {
215
+ "description": "Schema of the models",
216
+ "content": {
217
+ "application/json": {
218
+ "schema": {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "object",
222
+ "properties": {
223
+ "id": {
224
+ "type": "integer",
225
+ },
226
+ "created_at": {
227
+ "type": "string",
228
+ "format": "date-time",
229
+ },
230
+ "updated_at": {
231
+ "type": "string",
232
+ "format": "date-time",
233
+ },
234
+ },
235
+ },
236
+ },
237
+ },
238
+ },
239
+ },
240
+ },
241
+ },
242
+ },
243
+ "/info/dsl": {
244
+ "get": {
245
+ "summary": "DSL",
246
+ "description": "Returns the DSL of the models",
247
+ "tags": ["Info"],
248
+ "security": [
249
+ "bearerAuth": [],
250
+ ],
251
+ "responses": {
252
+ "200": {
253
+ "description": "DSL of the models",
254
+ "content": {
255
+ "application/json": {
256
+ "schema": {
257
+ "type": "object",
258
+ "properties": {
259
+ "id": {
260
+ "type": "integer",
261
+ },
262
+ "created_at": {
263
+ "type": "string",
264
+ "format": "date-time",
265
+ },
266
+ "updated_at": {
267
+ "type": "string",
268
+ "format": "date-time",
269
+ },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ },
278
+ "/info/translations": {
279
+ "get": {
280
+ "summary": "Translations",
281
+ "description": "Returns the translations of the entire App",
282
+ "tags": ["Info"],
283
+ "security": [
284
+ "bearerAuth": [],
285
+ ],
286
+ "responses": {
287
+ "200": {
288
+ "description": "Translations",
289
+ "content": {
290
+ "application/json": {
291
+ "schema": {
292
+ "type": "object",
293
+ "properties": {
294
+ "key": {
295
+ "type": "string",
296
+ },
297
+ "value": {
298
+ "type": "string",
299
+ },
300
+ },
301
+ },
302
+ },
303
+ },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ "/info/settings": {
309
+ "get": {
310
+ "summary": "Settings",
311
+ "description": "Returns the settings of the App",
312
+ "tags": ["Info"],
313
+ "security": [
314
+ "bearerAuth": [],
315
+ ],
316
+ "responses": {
317
+ "200": {
318
+ "description": "Settings",
319
+ "content": {
320
+ "application/json": {
321
+ "schema": {
322
+ "type": "object",
323
+ "properties": {
324
+ "ns": {
325
+ "type": "object",
326
+ "properties": {
327
+ "key": {
328
+ "type": "string",
329
+ },
330
+ "value": {
331
+ "type": "string",
332
+ },
333
+ },
334
+ },
335
+ },
336
+ },
337
+ },
338
+ },
339
+ },
340
+ },
341
+ },
342
+ },
343
+ "/info/swagger": {
344
+ "get": {
345
+ "summary": "Swagger",
346
+ "description": "Returns the self generated Swagger for all the models in the App.",
347
+ "tags": ["Info"],
348
+ "responses": {
349
+ "200": {
350
+ "description": "Swagger",
351
+ "content": {
352
+ "application/json": {
353
+ "schema": {
354
+ "type": "object",
355
+ "properties": {
356
+ "id": {
357
+ "type": "integer",
358
+ },
359
+ "created_at": {
360
+ "type": "string",
361
+ "format": "date-time",
362
+ },
363
+ "updated_at": {
364
+ "type": "string",
365
+ "format": "date-time",
366
+ },
367
+ },
368
+ },
369
+ },
370
+ },
371
+ },
372
+ },
373
+ },
374
+ },
375
+ }
376
+ ApplicationRecord.subclasses.sort_by { |d| d.to_s }.each do |d|
377
+ # Only if current user can read the model
378
+ if true # can? :read, d
379
+ model = d.to_s.underscore.tableize
380
+ # CRUD and Search endpoints
381
+ pivot["/#{model}"] = {
382
+ "get": {
383
+ "summary": "Index",
384
+ "description": "Returns the list of #{model}",
385
+ "tags": [model.classify],
386
+ "security": [
387
+ "bearerAuth": [],
388
+ ],
389
+ "responses": {
390
+ "200": {
391
+ "description": "List of #{model}",
392
+ "content": {
393
+ "application/json": {
394
+ "schema": {
395
+ "type": "array",
396
+ "items": {
397
+ "type": "object",
398
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
399
+ },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ "404": {
405
+ "description": "No #{model} found",
406
+ },
407
+ },
408
+ },
409
+ "post": {
410
+ "summary": "Create",
411
+ "description": "Creates a new #{model}",
412
+ "tags": [model.classify],
413
+ "security": [
414
+ "bearerAuth": [],
415
+ ],
416
+ "requestBody": {
417
+ "content": {
418
+ "application/json": {
419
+ "schema": {
420
+ "type": "object",
421
+ "properties": {
422
+ "#{model.singularize}": {
423
+ "type": "object",
424
+ "properties": create_properties_from_model(d, {}, true),
425
+ },
426
+ },
427
+ },
428
+ },
429
+ },
430
+ },
431
+ "responses": {
432
+ "200": {
433
+ "description": "#{model} Created",
434
+ "content": {
435
+ "application/json": {
436
+ "schema": {
437
+ "type": "object",
438
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
439
+ },
440
+ },
441
+ },
442
+ },
443
+ },
444
+ },
445
+ }
446
+ # Non CRUD or Search, but custom, usually bulk operations endpoints
447
+ new_custom_actions = ("Endpoints::#{d.model_name.name}".constantize.instance_methods(false) rescue [])
448
+ Rails.logger.debug "New Custom Actions (#{d.model_name.name}): #{new_custom_actions}"
449
+ new_custom_actions.each do |action|
450
+ openapi_definition = "Endpoints::#{d.model_name.name}".constantize.definitions[d.model_name.name][action.to_sym] rescue []
451
+
452
+ # Add the tag to the openapi definition
453
+ openapi_definition.each do |k, v|
454
+ v[:tags] = [d.model_name.name]
455
+ end
456
+
457
+ # Check if any HTTP method has path parameters with 'in: path', and if so, add {id} to the path
458
+ path = "/#{model}/custom_action/#{action}"
459
+ has_path_params = openapi_definition.any? { |_k, v| v.is_a?(Hash) && v[:parameters]&.any? { |p| p[:in] == 'path' } }
460
+ path += "/{id}" if has_path_params
461
+
462
+ pivot[path] = openapi_definition if openapi_definition
463
+ end
464
+ pivot["/#{model}/search"] = {
465
+ # Complex queries are made using ranskac search via a post endpoint
466
+ "post": {
467
+ "summary": "Search",
468
+ "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.",
469
+ "tags": [model.classify],
470
+ "security": [
471
+ "bearerAuth": [],
472
+ ],
473
+ "requestBody": {
474
+ "content": {
475
+ "application/json": {
476
+ "schema": {
477
+ "type": "object",
478
+ "properties": {
479
+ "q": {
480
+ "type": "object",
481
+ "properties": {
482
+ "name_or_description_cont": {
483
+ "type": "string",
484
+ },
485
+ "first_name_eq": {
486
+ "type": "string",
487
+ },
488
+ },
489
+ },
490
+ },
491
+ },
492
+ },
493
+ },
494
+ },
495
+ "responses": {
496
+ "200": {
497
+ "description": "List of #{model}",
498
+ "content": {
499
+ "application/json": {
500
+ "schema": {
501
+ "type": "array",
502
+ "items": {
503
+ "type": "object",
504
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
505
+ },
506
+ },
507
+ },
508
+ },
509
+ },
510
+ "404": {
511
+ "description": "No #{model} found",
512
+ },
513
+ },
514
+ },
515
+ }
516
+ pivot["/#{model}/{id}"] = {
517
+ "put": {
518
+ "summary": "Update",
519
+ "description": "Updates the complete #{model}",
520
+ "parameters": [
521
+ {
522
+ "name": "id",
523
+ "in": "path",
524
+ "required": true,
525
+ "schema": {
526
+ "type": "integer",
527
+ },
528
+ },
529
+ ],
530
+ "tags": [model.classify],
531
+ "security": [
532
+ "bearerAuth": [],
533
+ ],
534
+ "requestBody": {
535
+ "content": {
536
+ "application/json": {
537
+ "schema": {
538
+ "type": "object",
539
+ "properties": {
540
+ "#{model.singularize}": {
541
+ "type": "object",
542
+ "properties": create_properties_from_model(d, {}, true),
543
+ },
544
+ },
545
+ },
546
+ },
547
+ },
548
+ },
549
+ "responses": {
550
+ "200": {
551
+ "description": "#{model} Updated",
552
+ "content": {
553
+ "application/json": {
554
+ "schema": {
555
+ "type": "object",
556
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
557
+ },
558
+ },
559
+ },
560
+ },
561
+ "404": {
562
+ "description": "No #{model} found",
563
+ },
564
+ },
565
+ },
566
+ "patch": {
567
+ "summary": "Patch",
568
+ "description": "Updates the partial #{model}",
569
+ "parameters": [
570
+ {
571
+ "name": "id",
572
+ "in": "path",
573
+ "required": true,
574
+ "schema": {
575
+ "type": "integer",
576
+ },
577
+ },
578
+ ],
579
+ "tags": [model.classify],
580
+ "security": [
581
+ "bearerAuth": [],
582
+ ],
583
+ "requestBody": {
584
+ "content": {
585
+ "application/json": {
586
+ "schema": {
587
+ "type": "object",
588
+ "properties": {
589
+ "#{model.singularize}": {
590
+ "type": "object",
591
+ "properties": create_properties_from_model(d, {}, true),
592
+ },
593
+ },
594
+ },
595
+ },
596
+ },
597
+ },
598
+ "responses": {
599
+ "200": {
600
+ "description": "#{model} Patched",
601
+ "content": {
602
+ "application/json": {
603
+ "schema": {
604
+ "type": "object",
605
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
606
+ },
607
+ },
608
+ },
609
+ },
610
+ "404": {
611
+ "description": "No #{model} found",
612
+ },
613
+ },
614
+ },
615
+ "delete": {
616
+ "summary": "Delete",
617
+ "description": "Deletes the #{model}",
618
+ "parameters": [
619
+ {
620
+ "name": "id",
621
+ "in": "path",
622
+ "required": true,
623
+ "schema": {
624
+ "type": "integer",
625
+ },
626
+ },
627
+ ],
628
+ "tags": [model.classify],
629
+ "security": [
630
+ "bearerAuth": [],
631
+ ],
632
+ "responses": {
633
+ "200": {
634
+ "description": "#{model} Deleted",
635
+ },
636
+ "404": {
637
+ "description": "No #{model} found",
638
+ },
639
+ },
640
+ },
641
+ "get": {
642
+ "summary": "Show",
643
+ "description": "Shows the #{model}",
644
+ "parameters": [
645
+ {
646
+ "name": "id",
647
+ "in": "path",
648
+ "required": true,
649
+ "schema": {
650
+ "type": "integer",
651
+ },
652
+ },
653
+ ],
654
+ "tags": [model.classify],
655
+ "security": [
656
+ "bearerAuth": [],
657
+ ],
658
+ "responses": {
659
+ "200": {
660
+ "description": "Show #{model}",
661
+ "content": {
662
+ "application/json": {
663
+ "schema": {
664
+ "type": "object",
665
+ "properties": create_properties_from_model(d, (d.json_attrs rescue {})),
666
+ },
667
+ },
668
+ },
669
+ },
670
+ "404": {
671
+ "description": "No #{model} found",
672
+ },
673
+ },
674
+ },
675
+ }
676
+ # d.columns_hash.each_pair do |key, val|
677
+ # pivot[model][key] = val.type unless key.ends_with? "_id"
678
+ # end
679
+ # # Only application record descendants in order to have a clean schema
680
+ # pivot[model][:associations] ||= {
681
+ # has_many: d.reflect_on_all_associations(:has_many).map { |a|
682
+ # a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
683
+ # }.compact,
684
+ # belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
685
+ # a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
686
+ # }.compact
687
+ # }
688
+ # pivot[model][:methods] ||= (d.instance_methods(false).include?(:json_attrs) && !d.json_attrs.blank?) ? d.json_attrs[:methods] : nil
689
+ end
690
+ end
691
+ pivot
692
+ end
693
+
694
+ def description
695
+ info_description
696
+ end
697
+
698
+ private
699
+
700
+ def info_description
701
+ info = <<-MARKDOWN
702
+
703
+ ## About this API Documentation
704
+
705
+ 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.
706
+
707
+ 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).
708
+
709
+ The documentation starts from the authentication mechanism, integrated with the details from the provided code.
710
+ 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.
711
+
712
+ ---
713
+
714
+ ## Authentication & Token Management
715
+
716
+ The API implements a **stateless JWT (JSON Web Token)** authentication mechanism. It consists of two distinct phases:
717
+
718
+ 1. **Initial Authentication:** Exchanging credentials for the first Token.
719
+ 2. **Session Maintenance:** Using a **Sliding Expiration** strategy where every subsequent successful request issues a fresh token.
720
+
721
+ ### 1. Initial Authentication (Login)
722
+
723
+ 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.
724
+
725
+ #### Request
726
+
727
+ **Endpoint:** `POST /api/v2/authenticate`
728
+
729
+ **Body:**
730
+
731
+ ```json
732
+ {
733
+ "auth": {
734
+ "email": "admin@example.com",
735
+ "password": "Change#1"
736
+ }
737
+ }
738
+
739
+ ```
740
+
741
+ #### Response
742
+
743
+ Upon successful authentication, the server returns two critical pieces of data:
744
+
745
+ 1. **Response Body:** Contains the User object details.
746
+ 2. **Response Headers:** Contains the initial JWT in the `token` header.
747
+
748
+ **Example Body:**
749
+
750
+ ```json
751
+ {
752
+ "id": 219,
753
+ "email": "admin@example.com",
754
+ "created_at": "2025-12-10T07:57:54.336Z",
755
+ "admin": true,
756
+ "locked": false,
757
+ "locale": "en",
758
+ // ... other user attributes
759
+ "roles": []
760
+ }
761
+
762
+ ```
763
+
764
+ **Example Headers:**
765
+ Note the presence of the `token` header.
766
+
767
+ ```http
768
+ HTTP/1.1 200 OK
769
+ content-type: application/json; charset=utf-8
770
+ token: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyMTksImV4cCI6MTc2NjQ3ODMwN30.I0qJzOwA0Jxx0frL5-9jVH2PsakdZjSEY8Kqb9S3GKo
771
+ x-request-id: 113cad63-11f8-4daf-b684-19322a053bcc
772
+ ...
773
+
774
+ ```
775
+
776
+ ---
777
+
778
+ ### 2. Sliding Expiration (Token Renewal)
779
+
780
+ 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.
781
+
782
+ #### The Renewal Mechanism
783
+
784
+ 1. **Client Request:** The client sends the *current* token in the `Authorization` header.
785
+ ```http
786
+ Authorization: Bearer <Current_Token>
787
+
788
+ ```
789
+
790
+
791
+ 2. **Verification:** The `authenticate_request` method verifies the token. If valid, it sets `@current_user`.
792
+ 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.
793
+ ```ruby
794
+ response.set_header("Token", JsonWebToken.encode(user_id: current_user.id))
795
+
796
+ ```
797
+
798
+ #### Client-Side Implementation Guide
799
+
800
+ To maintain a valid session, the client must implement an interceptor to handle token rotation:
801
+
802
+ 1. **Login:** Call `/api/v2/authenticate` and store the `token` from the response header.
803
+ 2. **Subsequent Requests:** Attach the stored token to the `Authorization: Bearer ...` header.
804
+ 3. **Update Storage:**
805
+ * Check every response for a `Token` header.
806
+ * **If present:** Immediately replace the stored token with this new value.
807
+ * **If missing:** Continue using the existing token (unless the response was a 401/403 error).
808
+
809
+ #### Failure Scenarios
810
+
811
+ If the `authenticate_request` fails (e.g., token expired, invalid signature):
812
+
813
+ * The controller returns an unauthenticated error (`unauthenticated!`).
814
+ * The execution halts, and the line generating the new header is never reached.
815
+ * **Result:** The client receives a 401 error and **no new token**, signaling that the user must perform the **Initial Authentication** (login) again.
816
+
817
+ ---
818
+
819
+ ## API Documentation: Search, Filtering, and Pagination Parameters
820
+
821
+ ### 1. Pagination and Counting
822
+
823
+ These parameters control the amount of data returned and navigation through result pages.
824
+
825
+ * **`page`** (Integer): Indicates the page number to retrieve. If omitted, pagination is not applied (or defaults to the model's Kaminari configuration).
826
+ * **`per`** (Integer): Indicates the number of records per page. Works in conjunction with `page`.
827
+ * **`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.
828
+
829
+ ### 2. Field Selection
830
+
831
+ 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`.
832
+
833
+ * **`a`** (or `json_attrs`): An object or hash defining the output JSON structure.
834
+ * **`only`**: Array of strings. Returns only the specified attributes of the main model.
835
+ * **`methods`**: Array of strings. Includes model methods that are not database columns.
836
+ * **`include`**: Object for including relationships (associations). Associations can also have their own `only` or `methods`.
837
+
838
+ ### 3. Custom Actions
839
+
840
+ * **`do`**: Specifies a custom action (`custom_action`) to execute on the model instead of the standard `index`.
841
+ * Format: `?do=action_name` or `?do=action_name-token`.
842
+ * The controller will look for a class method `custom_action_action_name` or a module `Endpoints::Model`.
843
+
844
+ ### 4. Filters and Sorting (Ransack)
845
+
846
+ 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.
847
+
848
+ Basic structure: `q[field_name_predicate]=value`
849
+
850
+ #### Common Predicates (Suffixes)
851
+
852
+ * `_eq`: Equal to (e.g., `status_eq`).
853
+ * `_cont`: Contains (LIKE %value%, case insensitive).
854
+ * `_start`: Starts with.
855
+ * `_end`: Ends with.
856
+ * `_gt` / `_lt`: Greater than / Less than (for numbers or dates).
857
+ * `_gteq` / `_lteq`: Greater than or equal to / Less than or equal to.
858
+ * `_in`: Included in a list (accepts an array).
859
+ * `_present`: If set to `1` or `true`, filters for non-null values. `_blank` for nulls.
860
+
861
+ #### Sorting
862
+
863
+ * `s`: Defines the sorting order. Format: `field_name asc` or `field_name desc`.
864
+
865
+ ---
866
+
867
+ ## Practical Examples
868
+
869
+ Below are two usage scenarios to achieve the same result: a **GET** request (URL parameters) and a **POST** request (JSON Body parameters).
870
+
871
+ #### Scenario A: Simple Search and Pagination
872
+
873
+ **Goal:** Find users whose name contains "Mario", paginated (page 2, 10 per page).
874
+
875
+ ##### 1. Using GET (Query String)
876
+
877
+ Parameters are "flattened" and encoded in the URL.
878
+
879
+ ```text
880
+ GET /api/v2/users?q[name_cont]=Mario&page=2&per=10
881
+
882
+ ```
883
+
884
+ ##### 2. Using POST (JSON Body)
885
+
886
+ Ideal for complex searches to avoid exceeding URL length limits.
887
+
888
+ ```json
889
+ POST /api/v2/users/search
890
+ Content-Type: application/json
891
+
892
+ {
893
+ "q": {
894
+ "name_cont": "Mario"
895
+ },
896
+ "page": 2,
897
+ "per": 10
898
+ }
899
+
900
+ ```
901
+
902
+ ---
903
+
904
+ #### Scenario B: Advanced Search, Sorting, and Field Selection
905
+
906
+ **Goal:**
907
+
908
+ 1. Search for orders (`orders`) where `total_price` is greater than 50.
909
+ 2. Belonging to a user (`user`) whose email ends with `@test.com`.
910
+ 3. Sort by creation date descending.
911
+ 4. Return only the `id` and `total_price` of the order, including the `email` of the associated user.
912
+
913
+ ##### 1. Using GET (Query String)
914
+
915
+ Note the square bracket syntax for nested structures (`q`, `a`).
916
+
917
+ ```text
918
+ 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
919
+
920
+ ```
921
+
922
+ ##### 2. Using POST (JSON Body)
923
+
924
+ Much more readable for nested structures like `a` (json_attrs).
925
+
926
+ ```json
927
+ POST /api/v2/orders/search
928
+ Content-Type: application/json
929
+
930
+ {
931
+ "q": {
932
+ "total_price_gt": 50,
933
+ "user_email_end": "@test.com",
934
+ "s": "created_at desc"
935
+ },
936
+ "a": {
937
+ "only": ["id", "total_price"],
938
+ "include": {
939
+ "user": {
940
+ "only": ["email"]
941
+ }
942
+ }
943
+ }
944
+ }
945
+
946
+ ```
947
+
948
+ ---
949
+
950
+ #### Scenario C: Multiple Search (OR) and Arrays
951
+
952
+ **Goal:** Find products where status is "new" **OR** "refurbished" (using `_in`).
953
+
954
+ ##### 1. Using GET (Query String)
955
+
956
+ To pass an array in GET, repeat the empty square brackets `[]`.
957
+
958
+ ```text
959
+ GET /api/v2/products?q[status_in][]=new&q[status_in][]=refurbished
960
+
961
+ ```
962
+
963
+ ##### 2. Using POST (JSON Body)
964
+
965
+ ```json
966
+ POST /api/v2/products/search
967
+ Content-Type: application/json
968
+
969
+ {
970
+ "q": {
971
+ "status_in": ["new", "refurbished"]
972
+ }
973
+ }
974
+
975
+ ```
976
+
977
+ ---
978
+
979
+ #### Scenario D: Count Only
980
+
981
+ **Goal:** Know how many users are active without downloading the data.
982
+
983
+ ##### 1. Using GET
984
+
985
+ ```text
986
+ GET /api/v2/users?q[active_eq]=true&count=true
987
+
988
+ ```
989
+
990
+ ##### 2. Using POST
991
+
992
+ ```json
993
+ POST /api/v2/users/search
994
+ Content-Type: application/json
995
+
996
+ {
997
+ "q": {
998
+ "active_eq": true
999
+ },
1000
+ "count": true
1001
+ }
1002
+
1003
+ ```
1004
+
1005
+ **Expected Response:**
1006
+
1007
+ ```json
1008
+ {
1009
+ "count": 156
1010
+ }
1011
+
1012
+ ```
1013
+
1014
+ ## ActiveStorage Integration: React Frontend & Rails Backend
1015
+
1016
+ ### Overview
1017
+
1018
+ This guide explains how to handle file uploads (via Camera or Gallery) and attachment deletions using a **React** frontend and a **Ruby on Rails** backend.
1019
+
1020
+ The Rails model uses a virtual attribute strategy for deletion:
1021
+
1022
+ * **Upload:** handled via `has_many_attached :assets`
1023
+ * **Deletion:** handled via `attr_accessor :remove_assets`
1024
+
1025
+ ---
1026
+
1027
+ ### 1. Handling File Objects (No "Paths" needed)
1028
+
1029
+ In a web/mobile context (React Web or PWA), you do not need a file system path. When a user takes a photo or selects a file, the browser creates a native **`File`** object (a type of `Blob`).
1030
+
1031
+ You must send this binary object to the backend using **`FormData`**.
1032
+
1033
+ #### React Component Example
1034
+
1035
+ This component handles:
1036
+
1037
+ 1. **File Input:** Supports both gallery selection and direct camera capture on mobile.
1038
+ 2. **FormData:** Constructs the payload correctly for Rails.
1039
+ 3. **API Call:** Sends the data via `fetch`.
1040
+
1041
+ ```jsx
1042
+ import React, { useState } from 'react';
1043
+
1044
+ const ProductForm = () => {
1045
+ const [title, setTitle] = useState('');
1046
+ const [selectedFiles, setSelectedFiles] = useState([]);
1047
+
1048
+ // Handle file selection
1049
+ const handleFileChange = (event) => {
1050
+ // event.target.files is a FileList; convert to Array for convenience
1051
+ const filesArray = Array.from(event.target.files);
1052
+ setSelectedFiles(filesArray);
1053
+ };
1054
+
1055
+ // Handle form submission
1056
+ const handleSubmit = async (event) => {
1057
+ event.preventDefault();
1058
+
1059
+ // 1. Create the FormData object
1060
+ const formData = new FormData();
1061
+
1062
+ // 2. Append text fields
1063
+ formData.append('product[title]', title);
1064
+
1065
+ // 3. Append FILES
1066
+ // It is crucial to use 'product[assets][]' with brackets.
1067
+ // This tells Rails to treat it as an array of attachments.
1068
+ selectedFiles.forEach((file) => {
1069
+ formData.append('product[assets][]', file);
1070
+ });
1071
+
1072
+ try {
1073
+ const response = await fetch('http://localhost:3000/api/products', {
1074
+ method: 'POST',
1075
+ // IMPORTANT NOTE:
1076
+ // When using FormData, do NOT set 'Content-Type': 'application/json'
1077
+ // and do NOT manually set 'multipart/form-data'.
1078
+ // The browser will automatically set the header with the correct 'boundary'.
1079
+ body: formData,
1080
+ });
1081
+
1082
+ if (response.ok) {
1083
+ console.log("Upload successful!");
1084
+ // Reset form or redirect...
1085
+ } else {
1086
+ console.error("Upload error");
1087
+ }
1088
+ } catch (error) {
1089
+ console.error("Network error:", error);
1090
+ }
1091
+ };
1092
+
1093
+ return (
1094
+ <form onSubmit={handleSubmit} style={{ padding: '20px' }}>
1095
+ <div>
1096
+ <label>Product Title:</label>
1097
+ <input
1098
+ type="text"
1099
+ value={title}
1100
+ onChange={(e) => setTitle(e.target.value)}
1101
+ />
1102
+ </div>
1103
+
1104
+ <div style={{ marginTop: '20px' }}>
1105
+ <label>Photos (Camera or Gallery):</label>
1106
+ {/* accept="image/*": Accepts only images.
1107
+ capture="environment": On mobile, opens the rear camera directly.
1108
+ Remove 'capture' if you want the user to choose between Gallery and Camera.
1109
+ multiple: Allows selecting multiple photos.
1110
+ */}
1111
+ <input
1112
+ type="file"
1113
+ accept="image/*"
1114
+ multiple
1115
+ onChange={handleFileChange}
1116
+ />
1117
+ </div>
1118
+
1119
+ <button type="submit" style={{ marginTop: '20px' }}>
1120
+ Save Product
1121
+ </button>
1122
+ </form>
1123
+ );
1124
+ };
1125
+
1126
+ export default ProductForm;
1127
+
1128
+ ```
1129
+
1130
+ ---
1131
+
1132
+ ### 2. Key Implementation Details
1133
+
1134
+ #### A. The `capture` Attribute
1135
+
1136
+ * `<input type="file" capture="environment" />`: Opens the **rear camera** directly on iOS/Android.
1137
+ * `<input type="file" capture="user" />`: Opens the **front camera** (selfie mode).
1138
+ * **No `capture` attribute** (but with `accept="image/*"`): The device will prompt the user: *"Take Photo or Photo Library?"*. This is often the best UX.
1139
+
1140
+ #### B. The `forEach` Loop
1141
+
1142
+ You cannot pass an array directly into `FormData` (e.g., `formData.append('key', myArray)` will not work).
1143
+ Rails expects multiple values for the same key. You must append each file individually:
1144
+
1145
+ ```javascript
1146
+ // Correct
1147
+ files.forEach(file => formData.append('product[assets][]', file));
1148
+
1149
+ ```
1150
+
1151
+ #### C. The Content-Type Header
1152
+
1153
+ This is a common pitfall. When using `fetch` or `axios` with a `FormData` body, **do not set the Content-Type header manually**.
1154
+ The browser must generate it automatically to include the boundary:
1155
+ `Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...`
1156
+
1157
+ ---
1158
+
1159
+ ## 3. Handling Deletion (PATCH Request)
1160
+
1161
+ To remove specific attachments using the `remove_assets` virtual attribute defined in your Rails model, send the **Attachment IDs** (not the file objects).
1162
+
1163
+ ```javascript
1164
+ const handleUpdate = async () => {
1165
+ const formData = new FormData();
1166
+
1167
+ // 1. Add new files (if any)
1168
+ newFiles.forEach(file => formData.append('product[assets][]', file));
1169
+
1170
+ // 2. Add IDs to remove
1171
+ // (e.g., idsToRemove is an array like [12, 45])
1172
+ idsToRemove.forEach(id => formData.append('product[remove_assets][]', id));
1173
+
1174
+ await fetch(`http://localhost:3000/api/products/${productId}`, {
1175
+ method: 'PATCH',
1176
+ body: formData
1177
+ });
1178
+ };
1179
+
1180
+ ```
1181
+
1182
+ ### 4. Bare Metal HTTP Requests
1183
+
1184
+ If you were to inspect the network traffic or manually construct the request (e.g., using raw sockets or a tool like Postman/Insomnia), this is exactly what the payload looks like "over the wire."
1185
+
1186
+ #### A. POST Request (Upload)
1187
+
1188
+ Notice how `multipart/form-data` uses a **boundary** string (randomly generated by the client) to separate different fields.
1189
+
1190
+ ```http
1191
+ POST /api/products HTTP/1.1
1192
+ Host: localhost:3000
1193
+ Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
1194
+
1195
+ ------WebKitFormBoundary7MA4YWxkTrZu0gW
1196
+ Content-Disposition: form-data; name="product[title]"
1197
+
1198
+ My New Product
1199
+ ------WebKitFormBoundary7MA4YWxkTrZu0gW
1200
+ Content-Disposition: form-data; name="product[assets][]"; filename="camera_shot.jpg"
1201
+ Content-Type: image/jpeg
1202
+
1203
+ (Binary image data goes here...)
1204
+ ------WebKitFormBoundary7MA4YWxkTrZu0gW
1205
+ Content-Disposition: form-data; name="product[assets][]"; filename="gallery_photo.png"
1206
+ Content-Type: image/png
1207
+
1208
+ (Binary image data goes here...)
1209
+ ------WebKitFormBoundary7MA4YWxkTrZu0gW--
1210
+
1211
+ ```
1212
+
1213
+ #### B. PATCH Request (Deletion via IDs)
1214
+
1215
+ When sending the `remove_assets` array via `FormData`, the key is repeated for every ID. This is how HTTP handles arrays in form data.
1216
+
1217
+ ```http
1218
+ PATCH /api/products/100 HTTP/1.1
1219
+ Host: localhost:3000
1220
+ Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXyZ123
1221
+
1222
+ ------WebKitFormBoundaryXyZ123
1223
+ Content-Disposition: form-data; name="product[remove_assets][]"
1224
+
1225
+ 12
1226
+ ------WebKitFormBoundaryXyZ123
1227
+ Content-Disposition: form-data; name="product[remove_assets][]"
1228
+
1229
+ 45
1230
+ ------WebKitFormBoundaryXyZ123--
1231
+
1232
+ ```
1233
+ MARKDOWN
1234
+ info
1235
+ end
1236
+ end
1237
+ end
1238
+ end