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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 010f1acfbe42371a8465ba5eeae1f6a5cc0d8c7d5891c30b52ed2c9056bfd7d0
4
- data.tar.gz: fb81afbec2d0ce91297c68b212a97e213c0b0d7cf15630c82af753f5511ec1f7
3
+ metadata.gz: b97e79e1bc34847a05fd1c9ac004c0f64d15203b2a803d176559bfdcc2b3cf4e
4
+ data.tar.gz: 6c48764ef66785db7b5793bd0bde4526233f39ae10650ec76c98799f0ac5efef
5
5
  SHA512:
6
- metadata.gz: '01941ccd88639849a80e1068266b083911915793f0513835e6caf2c61b718d7c4c80d0c57c4a59f25f4a64ddf85a728d18fccada0cf7e53e813b39293b26b339'
7
- data.tar.gz: e9243797670a8dba2dbd87a2e79571dbb5e39dea8fa26379934a08fe74137db7bb053fc73bbb63d811962397bff5e4cb64c11fdf7b65b5abcfe363827a7185df
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
@@ -18,6 +18,10 @@ Rails.application.routes.draw do
18
18
  get :openapi
19
19
  end
20
20
 
21
+ namespace :raw do
22
+ post :sql
23
+ end
24
+
21
25
  post "authenticate" => "authentication#authenticate"
22
26
  post ":ctrl/search" => 'application#index'
23
27
 
@@ -1,3 +1,3 @@
1
1
  module ModelDrivenApi
2
- VERSION = "3.2.4".freeze
2
+ VERSION = "3.2.6".freeze
3
3
  end
@@ -15,6 +15,8 @@ require 'deep_merge/rails_compat'
15
15
 
16
16
  require "model_driven_api/engine"
17
17
 
18
+ require "safe_sql_executor"
19
+
18
20
  module ModelDrivenApi
19
21
  def self.smart_merge src, dest
20
22
  src.deeper_merge! dest, {
@@ -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
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: 2024-07-22 00:00:00.000000000 Z
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.5.11
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.