model_driven_api 3.2.4 → 3.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/info_controller.rb +61 -0
- data/app/controllers/api/v2/raw_controller.rb +22 -0
- data/config/routes.rb +4 -0
- data/lib/model_driven_api/version.rb +1 -1
- data/lib/model_driven_api.rb +2 -0
- data/lib/safe_sql_executor.rb +20 -0
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b97e79e1bc34847a05fd1c9ac004c0f64d15203b2a803d176559bfdcc2b3cf4e
|
4
|
+
data.tar.gz: 6c48764ef66785db7b5793bd0bde4526233f39ae10650ec76c98799f0ac5efef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 925b9065f28afcfa64652a4262eccf1ad59f829c153f8b9950653331857c9020649633be0590e18c7acfe29aafa06118c839cbbce3e993085c77f98d4d7c4e59
|
7
|
+
data.tar.gz: 2ae0d12d73b4c23113d0f98d1829adf4e35ea8a29fa0370d7d7fdd0f8fae5fe3dfcbfab33a7bbd6e0b6a97b9e5a9c431bdcdef403114b2f1846d06a50f701fc9
|
@@ -198,6 +198,67 @@ class Api::V2::InfoController < Api::V2::ApplicationController
|
|
198
198
|
}
|
199
199
|
}
|
200
200
|
},
|
201
|
+
"/raw/sql": {
|
202
|
+
"post": {
|
203
|
+
"summary": "Raw SQL query execution of SELECT queries",
|
204
|
+
"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",
|
205
|
+
"tags": ["Raw"],
|
206
|
+
"security": [
|
207
|
+
"bearerAuth": []
|
208
|
+
],
|
209
|
+
"responses": {
|
210
|
+
"200": {
|
211
|
+
"description": "SQL Query Result",
|
212
|
+
"content": {
|
213
|
+
"application/json": {
|
214
|
+
"schema": {
|
215
|
+
"type": "array",
|
216
|
+
"items": {
|
217
|
+
"type": "object",
|
218
|
+
"properties": {
|
219
|
+
"json_agg": {
|
220
|
+
"type": "string"
|
221
|
+
}
|
222
|
+
}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
}
|
227
|
+
},
|
228
|
+
"400": {
|
229
|
+
"description": "SQL query must return a key called result otherwise cannot be parsed",
|
230
|
+
"content": {
|
231
|
+
"application/json": {
|
232
|
+
"schema": {
|
233
|
+
"type": "object",
|
234
|
+
"properties": {
|
235
|
+
"error": {
|
236
|
+
"type": "string"
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
|
244
|
+
},
|
245
|
+
"requestBody": {
|
246
|
+
"content": {
|
247
|
+
"application/json": {
|
248
|
+
"schema": {
|
249
|
+
"type": "object",
|
250
|
+
"properties": {
|
251
|
+
"query": {
|
252
|
+
"type": "string",
|
253
|
+
"example": "SELECT json_agg(u) FROM users u WHERE u.active = true;"
|
254
|
+
}
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
}
|
261
|
+
},
|
201
262
|
"/info/version": {
|
202
263
|
"get": {
|
203
264
|
"summary": "Version",
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# require 'model_driven_api/version'
|
2
|
+
class Api::V2::RawController < Api::V2::ApplicationController
|
3
|
+
# Info uses a different auth method: username and password
|
4
|
+
# skip_before_action :authenticate_request, only: [:version, :swagger, :openapi], raise: false
|
5
|
+
skip_before_action :extract_model
|
6
|
+
|
7
|
+
# api :GET, '/api/v2/raw/sql'
|
8
|
+
def sql
|
9
|
+
# if params is nil, render 400
|
10
|
+
render json: { error: "Query is required" }, status: 400 and return if params[:query].nil?
|
11
|
+
|
12
|
+
query = params[:query]
|
13
|
+
|
14
|
+
first_element = SafeSqlExecutor.execute_select(query).first rescue nil
|
15
|
+
|
16
|
+
# Error if first_element does not contain a key called results
|
17
|
+
render json: { error: "Query must return a key called result" }, status: 400 and return if first_element.nil? || !first_element.key?("result")
|
18
|
+
|
19
|
+
render json: first_element["result"], status: 200
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/config/routes.rb
CHANGED
data/lib/model_driven_api.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
module SafeSqlExecutor
|
2
|
+
def self.execute_select(query)
|
3
|
+
# Validate the query
|
4
|
+
validate_select_query(query)
|
5
|
+
|
6
|
+
# Execute the query
|
7
|
+
ActiveRecord::Base.connection.execute(query)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.validate_select_query(query)
|
13
|
+
sanitized_query = query.strip.gsub(/\s+/, " ").upcase
|
14
|
+
|
15
|
+
# Allow SELECT or WITH...SELECT queries
|
16
|
+
unless sanitized_query.match?(/^(WITH .+)?SELECT /)
|
17
|
+
raise ArgumentError, "Only SELECT queries (including with CTEs) are allowed"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: model_driven_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.2.
|
4
|
+
version: 3.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriele Tassoni
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-14 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: thecore_backend_commons
|
@@ -124,6 +123,7 @@ files:
|
|
124
123
|
- app/controllers/api/v2/application_controller.rb
|
125
124
|
- app/controllers/api/v2/authentication_controller.rb
|
126
125
|
- app/controllers/api/v2/info_controller.rb
|
126
|
+
- app/controllers/api/v2/raw_controller.rb
|
127
127
|
- app/controllers/api/v2/users_controller.rb
|
128
128
|
- app/models/endpoints/test_api.rb
|
129
129
|
- app/models/test_api.rb
|
@@ -146,13 +146,13 @@ files:
|
|
146
146
|
- lib/model_driven_api/engine.rb
|
147
147
|
- lib/model_driven_api/version.rb
|
148
148
|
- lib/non_crud_endpoints.rb
|
149
|
+
- lib/safe_sql_executor.rb
|
149
150
|
- lib/tasks/model_driven_api_tasks.rake
|
150
151
|
homepage: https://github.com/gabrieletassoni/model_driven_api
|
151
152
|
licenses:
|
152
153
|
- MIT
|
153
154
|
metadata:
|
154
155
|
allowed_push_host: https://rubygems.org
|
155
|
-
post_install_message:
|
156
156
|
rdoc_options: []
|
157
157
|
require_paths:
|
158
158
|
- lib
|
@@ -167,8 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
167
|
- !ruby/object:Gem::Version
|
168
168
|
version: '0'
|
169
169
|
requirements: []
|
170
|
-
rubygems_version: 3.
|
171
|
-
signing_key:
|
170
|
+
rubygems_version: 3.6.2
|
172
171
|
specification_version: 4
|
173
172
|
summary: Convention based RoR engine which uses DB schema introspection to create
|
174
173
|
REST APIs.
|