queryapigate 0.7.1__tar.gz → 0.9.0__tar.gz

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.
Files changed (65) hide show
  1. {queryapigate-0.7.1/queryapigate.egg-info → queryapigate-0.9.0}/PKG-INFO +18 -10
  2. {queryapigate-0.7.1 → queryapigate-0.9.0}/README.md +14 -8
  3. {queryapigate-0.7.1 → queryapigate-0.9.0}/pyproject.toml +2 -1
  4. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/__init__.py +1 -1
  5. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/app.py +141 -12
  6. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/bundle.py +2 -1
  7. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/config.py +78 -1
  8. queryapigate-0.9.0/queryapigate/definitions.py +98 -0
  9. queryapigate-0.9.0/queryapigate/engine.py +314 -0
  10. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/formats.py +6 -0
  11. queryapigate-0.9.0/queryapigate/mongotools.py +82 -0
  12. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/openapi.py +76 -5
  13. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/runners.py +85 -0
  14. queryapigate-0.9.0/queryapigate/schema.py +134 -0
  15. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/store.py +23 -0
  16. queryapigate-0.9.0/queryapigate/ui.py +4448 -0
  17. {queryapigate-0.7.1 → queryapigate-0.9.0/queryapigate.egg-info}/PKG-INFO +18 -10
  18. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/SOURCES.txt +3 -0
  19. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/requires.txt +4 -1
  20. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_audit_log.py +8 -1
  21. queryapigate-0.9.0/tests/test_mongotools.py +81 -0
  22. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_queryapigate.py +340 -3
  23. queryapigate-0.9.0/tests/test_settings.py +73 -0
  24. queryapigate-0.7.1/queryapigate/definitions.py +0 -66
  25. queryapigate-0.7.1/queryapigate/engine.py +0 -166
  26. queryapigate-0.7.1/queryapigate/schema.py +0 -99
  27. queryapigate-0.7.1/queryapigate/ui.py +0 -2655
  28. {queryapigate-0.7.1 → queryapigate-0.9.0}/LICENSE +0 -0
  29. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/__main__.py +0 -0
  30. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/apikeys.py +0 -0
  31. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cache.py +0 -0
  32. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cli.py +0 -0
  33. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/collection_admin.py +0 -0
  34. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/cors.py +0 -0
  35. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/errors.py +0 -0
  36. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/examples.py +0 -0
  37. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
  38. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/logging_setup.py +0 -0
  39. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/metrics.py +0 -0
  40. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/params.py +0 -0
  41. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/pool.py +0 -0
  42. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/postman.py +0 -0
  43. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/ratelimit.py +0 -0
  44. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate/sqltools.py +0 -0
  45. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/dependency_links.txt +0 -0
  46. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/entry_points.txt +0 -0
  47. {queryapigate-0.7.1 → queryapigate-0.9.0}/queryapigate.egg-info/top_level.txt +0 -0
  48. {queryapigate-0.7.1 → queryapigate-0.9.0}/setup.cfg +0 -0
  49. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_api_keys.py +0 -0
  50. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_cli.py +0 -0
  51. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collection_bundle.py +0 -0
  52. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collection_postman.py +0 -0
  53. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_collections.py +0 -0
  54. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_connection_encryption.py +0 -0
  55. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_cors_and_rate_limit.py +0 -0
  56. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_duckdb.py +0 -0
  57. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_examples.py +0 -0
  58. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_integration.py +0 -0
  59. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_jdbc.py +0 -0
  60. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_observability.py +0 -0
  61. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_params.py +0 -0
  62. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_release_check.py +0 -0
  63. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_response_cache.py +0 -0
  64. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_roles.py +0 -0
  65. {queryapigate-0.7.1 → queryapigate-0.9.0}/tests/test_sql_guard_fuzz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queryapigate
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Turn SQL into a REST API: run queries against MySQL, PostgreSQL, ClickHouse, SQLite or H2 over HTTP and get JSON, CSV, XML, YAML or XLSX back.
5
5
  Author-email: Anantha Raju C <arcswdev@gmail.com>
6
6
  License-Expression: FSL-1.1-MIT
@@ -41,10 +41,12 @@ Requires-Dist: JayDeBeApi>=1.2; extra == "h2"
41
41
  Requires-Dist: JPype1>=1.4; extra == "h2"
42
42
  Provides-Extra: duckdb
43
43
  Requires-Dist: duckdb>=1.0; extra == "duckdb"
44
+ Provides-Extra: mongo
45
+ Requires-Dist: pymongo>=4.0; extra == "mongo"
44
46
  Provides-Extra: encryption
45
47
  Requires-Dist: cryptography>=41; extra == "encryption"
46
48
  Provides-Extra: all
47
- Requires-Dist: queryapigate[clickhouse,duckdb,encryption,h2,mysql,postgres]; extra == "all"
49
+ Requires-Dist: queryapigate[clickhouse,duckdb,encryption,h2,mongo,mysql,postgres]; extra == "all"
48
50
  Provides-Extra: server
49
51
  Requires-Dist: gunicorn>=21; extra == "server"
50
52
  Provides-Extra: docs
@@ -74,8 +76,8 @@ Dynamic: license-file
74
76
  [**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
75
77
 
76
78
  <p align="center">
77
- <a href="https://www.youtube.com/watch?v=aiVbAkA2LS8">
78
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the 60-second demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
79
+ <a href="https://www.youtube.com/watch?v=WWImFj4m95o">
80
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
79
81
  </a>
80
82
  </p>
81
83
 
@@ -109,7 +111,7 @@ Crimson Garden,Comedy,PG-13,631,2
109
111
  <tr>
110
112
  <td width="25%">
111
113
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
112
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections tab listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
114
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections screen with a sidebar and All/Active/Inactive tabs, listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
113
115
  </a>
114
116
  <br><sub>Manage connections across every supported database, with live usage per connection</sub>
115
117
  </td>
@@ -121,13 +123,13 @@ Crimson Garden,Comedy,PG-13,631,2
121
123
  </td>
122
124
  <td width="25%">
123
125
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
124
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys tab showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
126
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys screen showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
125
127
  </a>
126
128
  <br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
127
129
  </td>
128
130
  <td width="25%">
129
131
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
130
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles tab listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
132
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles screen listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
131
133
  </a>
132
134
  <br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
133
135
  </td>
@@ -138,19 +140,19 @@ Crimson Garden,Comedy,PG-13,631,2
138
140
  <tr>
139
141
  <td width="33%">
140
142
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
141
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, connection, calling API key, rows, duration and request ID, with a status filter and search box">
143
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, calling API key, rows, duration and request ID, with a status filter and search box">
142
144
  </a>
143
145
  <br><sub>Run history per saved query: who called it, how long it took, and the request ID to find it in the logs</sub>
144
146
  </td>
145
147
  <td width="33%">
146
148
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
147
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit Log tab listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
149
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit log screen listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
148
150
  </a>
149
151
  <br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
150
152
  </td>
151
153
  <td width="33%">
152
154
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
153
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics tab with stat tiles for requests, error rate, active queries, pool and rate limiting, bar charts of requests by status and queries by connection, and a per-connection latency table">
155
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics screen with stat tiles for requests, error rate, active queries, pool and rate limiting, bars for requests by status and queries by connection, and a per-connection latency table">
154
156
  </a>
155
157
  <br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
156
158
  </td>
@@ -341,6 +343,11 @@ version management, API key management, an audit log of administrative changes,
341
343
  response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
342
344
  result, and one-click "copy as curl" / "copy as TSV" for any result.
343
345
 
346
+ The UI has a collapsible sidebar (Data, Access, Observability; `Ctrl`+`B` toggles it) and a read-only **Settings**
347
+ screen that shows every environment variable, its effective value and whether it was set or is the default (secrets are
348
+ reported as configured or not, never shown), with a "Copy as .env" button. Theme, table density and the default
349
+ result format are per-browser preferences.
350
+
344
351
  ```text
345
352
  Connect Database
346
353
  |
@@ -666,6 +673,7 @@ Clean up with `docker compose down -v`.
666
673
  | `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
667
674
  | `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
668
675
  | `/audit_log` | GET | Durable record of administrative changes - keys, connections, saved queries (admin only). |
676
+ | `/settings` | GET | The server's own configuration - each setting's effective value and whether it comes from the environment or the default; read-only, secrets never returned (admin only). |
669
677
  | `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
670
678
  | `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
671
679
  | `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
@@ -14,8 +14,8 @@
14
14
  [**Full documentation**](https://AnanthaRajuC.github.io/QueryAPIGate/)
15
15
 
16
16
  <p align="center">
17
- <a href="https://www.youtube.com/watch?v=aiVbAkA2LS8">
18
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the 60-second demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
17
+ <a href="https://www.youtube.com/watch?v=WWImFj4m95o">
18
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/demo-thumbnail.png" alt="Watch the demo: saved SQL queries as REST endpoints, scoped API keys, and the Move preview showing which keys gain or lose access" width="640">
19
19
  </a>
20
20
  </p>
21
21
 
@@ -49,7 +49,7 @@ Crimson Garden,Comedy,PG-13,631,2
49
49
  <tr>
50
50
  <td width="25%">
51
51
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png">
52
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections tab listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
52
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/connections.png" alt="Connections screen with a sidebar and All/Active/Inactive tabs, listing ClickHouse, MySQL, PostgreSQL and SQLite connections with active/inactive status and each connection's live usage: queries run, failures and average latency">
53
53
  </a>
54
54
  <br><sub>Manage connections across every supported database, with live usage per connection</sub>
55
55
  </td>
@@ -61,13 +61,13 @@ Crimson Garden,Comedy,PG-13,631,2
61
61
  </td>
62
62
  <td width="25%">
63
63
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png">
64
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys tab showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
64
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/api-keys.png" alt="API keys screen showing keys created from roles and scoped to a collection (shown as a dashed name/ tag), an expiring partner key, per-key rate limits, and each key's last-used time and live usage">
65
65
  </a>
66
66
  <br><sub>Per-key permissions: connections, specific saved queries, expiry, rate limits - and what each key has actually done</sub>
67
67
  </td>
68
68
  <td width="25%">
69
69
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png">
70
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles tab listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
70
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/roles.png" alt="Roles screen listing reusable permission templates - connections, read/write access, rate limit and IP allowlisting - with a New key from this shortcut">
71
71
  </a>
72
72
  <br><sub>Named roles: a reusable grant template, copied onto a key once at creation</sub>
73
73
  </td>
@@ -78,19 +78,19 @@ Crimson Garden,Comedy,PG-13,631,2
78
78
  <tr>
79
79
  <td width="33%">
80
80
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png">
81
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, connection, calling API key, rows, duration and request ID, with a status filter and search box">
81
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/saved-query-history.png" alt="A saved query's History tab: every run with its time, calling API key, rows, duration and request ID, with a status filter and search box">
82
82
  </a>
83
83
  <br><sub>Run history per saved query: who called it, how long it took, and the request ID to find it in the logs</sub>
84
84
  </td>
85
85
  <td width="33%">
86
86
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png">
87
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit Log tab listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
87
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/audit-log.png" alt="Audit log screen listing every administrative change - saved queries, roles and API keys created or updated - with an action filter and search box">
88
88
  </a>
89
89
  <br><sub>A durable audit log of every configuration change, filterable by action, actor and target</sub>
90
90
  </td>
91
91
  <td width="33%">
92
92
  <a href="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png">
93
- <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics tab with stat tiles for requests, error rate, active queries, pool and rate limiting, bar charts of requests by status and queries by connection, and a per-connection latency table">
93
+ <img src="https://raw.githubusercontent.com/AnanthaRajuC/QueryAPIGate/main/documentation/screenshots/metrics.png" alt="Metrics screen with stat tiles for requests, error rate, active queries, pool and rate limiting, bars for requests by status and queries by connection, and a per-connection latency table">
94
94
  </a>
95
95
  <br><sub>Live metrics with no Prometheus required - or import the bundled Grafana dashboard for history</sub>
96
96
  </td>
@@ -281,6 +281,11 @@ version management, API key management, an audit log of administrative changes,
281
281
  response inspection with a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric
282
282
  result, and one-click "copy as curl" / "copy as TSV" for any result.
283
283
 
284
+ The UI has a collapsible sidebar (Data, Access, Observability; `Ctrl`+`B` toggles it) and a read-only **Settings**
285
+ screen that shows every environment variable, its effective value and whether it was set or is the default (secrets are
286
+ reported as configured or not, never shown), with a "Copy as .env" button. Theme, table density and the default
287
+ result format are per-browser preferences.
288
+
284
289
  ```text
285
290
  Connect Database
286
291
  |
@@ -606,6 +611,7 @@ Clean up with `docker compose down -v`.
606
611
  | `/api_keys` | GET, POST | List / create scoped API keys (admin only). |
607
612
  | `/api_keys/<name>` | PATCH, DELETE | Update / revoke a scoped API key (admin only). |
608
613
  | `/audit_log` | GET | Durable record of administrative changes - keys, connections, saved queries (admin only). |
614
+ | `/settings` | GET | The server's own configuration - each setting's effective value and whether it comes from the environment or the default; read-only, secrets never returned (admin only). |
609
615
  | `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
610
616
  | `/metrics` | GET | Prometheus text-format metrics: request/query counts and latencies, pool occupancy, rate-limit rejections. |
611
617
  | `/ui` | GET | A small admin UI: manage connections and saved queries, run ad-hoc SQL. |
@@ -35,8 +35,9 @@ postgres = ["psycopg2-binary>=2.9"]
35
35
  clickhouse = ["clickhouse-driver>=0.2"]
36
36
  h2 = ["JayDeBeApi>=1.2", "JPype1>=1.4"] # also needs a Java runtime
37
37
  duckdb = ["duckdb>=1.0"] # Connection.interrupt()/InterruptException, relied on for query timeouts, verified from here
38
+ mongo = ["pymongo>=4.0"]
38
39
  encryption = ["cryptography>=41"] # only needed when QUERYAPIGATE_SECRET_KEY is set - see store.py's _get_fernet()
39
- all = ["queryapigate[mysql,postgres,clickhouse,h2,duckdb,encryption]"]
40
+ all = ["queryapigate[mysql,postgres,clickhouse,h2,duckdb,mongo,encryption]"]
40
41
  server = ["gunicorn>=21"]
41
42
  docs = ["mkdocs-material>=9,<10"]
42
43
  dev = ["queryapigate[all]", "ruff>=0.5", "openapi-spec-validator>=0.7", "hypothesis>=6.100", "mypy>=1.10",
@@ -1,5 +1,5 @@
1
1
  """QueryAPIGate - expose SQL databases as a REST API."""
2
- __version__ = '0.7.1'
2
+ __version__ = '0.9.0'
3
3
 
4
4
  from .app import create_app # noqa: E402 (app imports __version__)
5
5
 
@@ -21,6 +21,7 @@ from . import (
21
21
  examples,
22
22
  logging_setup,
23
23
  metrics,
24
+ mongotools,
24
25
  openapi,
25
26
  pool,
26
27
  postman,
@@ -411,6 +412,12 @@ def execute_sql_endpoint():
411
412
  if not data.get('connection_name'):
412
413
  raise ApiError('Connection name is missing')
413
414
  require_connection(data['connection_name'])
415
+ database = data.get('database')
416
+ if database and not g.permission.admin:
417
+ # Every other scoped-key boundary is per connection, never per database within one - a key granted a
418
+ # connection may run any SQL that connection's own configured database allows, but not redirect that
419
+ # same connection at a sibling database on the same server the admin never listed it for.
420
+ raise ApiError('Only the admin key may run a query against a different database on this connection', 403)
414
421
  params = get_object(data.get('params'), 'params')
415
422
  output_format = get_output_format(data)
416
423
  timeout = get_timeout(data)
@@ -420,7 +427,29 @@ def execute_sql_endpoint():
420
427
  limit, offset, page = get_pagination()
421
428
  result = engine.execute_sql(data['sql'], data['connection_name'], limit, offset, params, timeout,
422
429
  allow_writes=g.permission.allow_writes, key_name=caller_key_name(),
423
- allowed_write_ops=g.permission.allowed_write_ops)
430
+ allowed_write_ops=g.permission.allowed_write_ops, database=database)
431
+ return render(result, output_format, page, limit)
432
+
433
+
434
+ @bp.route('/execute_mongo', methods=['POST'])
435
+ def execute_mongo_endpoint():
436
+ """The Mongo sibling of /execute_sql - a find() query, ad-hoc, against a saved connection. Read-only,
437
+ full stop (BACKLOG #36: MongoDB support is find-only in this version) - unlike /execute_sql there is no
438
+ write permission to check."""
439
+ data = get_json_body()
440
+ if not data.get('collection'):
441
+ raise ApiError('Collection is missing')
442
+ if not data.get('connection_name'):
443
+ raise ApiError('Connection name is missing')
444
+ require_connection(data['connection_name'])
445
+ filter_doc = mongotools.validate_filter(get_object(data.get('filter'), 'filter'))
446
+ params = get_object(data.get('params'), 'params')
447
+ output_format = get_output_format(data)
448
+ timeout = get_timeout(data)
449
+ limit, offset, page = get_pagination()
450
+ result = engine.execute_mongo(data['collection'], filter_doc, data['connection_name'], limit, offset,
451
+ params, timeout, projection=data.get('projection'), sort=data.get('sort'),
452
+ key_name=caller_key_name())
424
453
  return render(result, output_format, page, limit)
425
454
 
426
455
 
@@ -477,19 +506,24 @@ def run_saved(ref, body, url_params):
477
506
  or apikeys.can_use_collection(g.permission, store.read_collection(content)))
478
507
  if not (granted_by_name and connection_name == saved.get('connection_name')):
479
508
  require_connection(connection_name)
509
+
510
+ raw = {**url_params, **get_object(body.get('params'), 'params'),
511
+ **get_object(body.get('placeholders'), 'placeholders')}
512
+ output_format = get_output_format(body)
513
+ timeout = get_timeout(body)
514
+
515
+ if saved.get('query_type') == 'mongo':
516
+ return run_saved_mongo(saved, path, number, ref, connection_name, raw, output_format, timeout)
517
+
480
518
  if not isinstance(saved.get('sql_query'), str):
481
519
  raise ApiError('Saved query has no SQL', 500)
482
520
  # Per-query write curation (apikeys.can_write_query()) only ever adds write reach for this one named
483
521
  # query on top of whatever the key's blanket allow_writes already grants - never the other way round.
484
522
  effective_allow_writes = g.permission.allow_writes or apikeys.can_write_query(g.permission, query_name)
485
523
 
486
- raw = {**url_params, **get_object(body.get('params'), 'params'),
487
- **get_object(body.get('placeholders'), 'placeholders')}
488
524
  used = set(sqltools.placeholder_names(saved['sql_query']))
489
525
  values = param_rules.resolve(saved.get('query_parameters'), raw, used=used)
490
526
  sql = sqltools.fill_placeholders(saved['sql_query'], values)
491
- output_format = get_output_format(body)
492
- timeout = get_timeout(body)
493
527
 
494
528
  if get_stream_flag():
495
529
  # No caching for a streamed export - caching would require materialising the whole body anyway,
@@ -529,6 +563,32 @@ def run_saved(ref, body, url_params):
529
563
  return response
530
564
 
531
565
 
566
+ def run_saved_mongo(saved, path, number, ref, connection_name, raw, output_format, timeout):
567
+ """The mongo query_type branch of run_saved(): a find() query instead of SQL. No caching (cache_ttl is
568
+ not yet supported for a mongo saved query) and no streaming (?stream=true) in this version - see
569
+ BACKLOG #36. Always read-only, same as the ad-hoc /execute_mongo."""
570
+ if get_stream_flag():
571
+ raise ApiError('Streaming is not supported for Mongo queries yet', 400)
572
+ used = set(mongotools.placeholder_names(saved.get('mongo_filter') or {}))
573
+ values = param_rules.resolve(saved.get('query_parameters'), raw, used=used)
574
+ filter_doc = mongotools.fill_placeholders(saved.get('mongo_filter') or {}, values)
575
+ limit, offset, page = get_pagination()
576
+ entry = {'executed_at': store.now(), 'connection_name': connection_name,
577
+ 'request_id': g.get('request_id'), 'key_name': caller_key_name()}
578
+ try:
579
+ result, elapsed_ms = engine.timed(engine.execute_mongo, saved['mongo_collection'], filter_doc,
580
+ connection_name, limit, offset, timeout=timeout,
581
+ projection=saved.get('mongo_projection'), sort=saved.get('mongo_sort'),
582
+ key_name=caller_key_name())
583
+ except ApiError as error:
584
+ store.record_execution(path, number, {**entry, 'status': 'error', 'error': error.message})
585
+ raise
586
+ response = render(result, output_format, page, limit) # sets g.serialization_ms - see render()
587
+ store.record_execution(path, number, {**entry, 'status': 'success', 'rows': len(result.rows),
588
+ 'duration_ms': elapsed_ms, 'serialization_ms': g.serialization_ms})
589
+ return response
590
+
591
+
532
592
  @bp.route('/execute_sql_from_file', methods=['POST'])
533
593
  @bp.route('/execute_sql_with_parameters_from_file', methods=['POST'])
534
594
  def execute_sql_from_file():
@@ -722,13 +782,63 @@ def update_connections():
722
782
  return jsonify({'message': 'Connections updated successfully'}), 200
723
783
 
724
784
 
785
+ @bp.route('/connections/databases', methods=['POST'])
786
+ def list_databases_route():
787
+ """Every database on the server a connection points at - saved, or not yet saved. Give 'name' alone for
788
+ an already-saved connection (Run SQL's database picker); give the connection's own fields (as
789
+ /connections/test does) to probe one that isn't saved yet - the New/Edit connection form's 'Default
790
+ database' dropdown - or to try changed fields before saving them. Admin only, like every other endpoint
791
+ that reveals the server's own configuration or reaches into a connection's server ahead of a real query."""
792
+ require_admin()
793
+ body = get_json_body()
794
+ if not isinstance(body, dict):
795
+ raise ApiError('Connection fields are missing')
796
+ if body.get('name') and not body.get('db'):
797
+ databases = schema.list_databases(body['name'])
798
+ else:
799
+ if body.get('db') not in config.SUPPORTED_DB_TYPES:
800
+ raise ApiError(f"'db' must be one of: {', '.join(config.SUPPORTED_DB_TYPES)}")
801
+ if body.get('password') == config.PASSWORD_MASK:
802
+ existing = store.read_connections().get(body.get('name') or '', {})
803
+ body = {**body, 'password': existing.get('password', '')}
804
+ databases = engine.list_databases(store.resolve_ad_hoc(body))
805
+ return jsonify({'databases': databases}), 200
806
+
807
+
808
+ @bp.route('/connections/test', methods=['POST'])
809
+ def test_connection_route():
810
+ """Try to connect with the given fields - not a saved connection's name, the fields themselves, exactly
811
+ as the New/Edit connection form has them right now. Nothing is written anywhere; a masked password
812
+ (unchanged from an existing connection's form) is resolved to the real stored value first, the same way
813
+ update_connections() already treats one."""
814
+ require_admin()
815
+ details = get_json_body()
816
+ if not isinstance(details, dict) or details.get('db') not in config.SUPPORTED_DB_TYPES:
817
+ raise ApiError(f"'db' must be one of: {', '.join(config.SUPPORTED_DB_TYPES)}")
818
+ if details.get('password') == config.PASSWORD_MASK:
819
+ existing = store.read_connections().get(details.get('name') or '', {})
820
+ details = {**details, 'password': existing.get('password', '')}
821
+ result = engine.test_connection(store.resolve_ad_hoc(details))
822
+ return jsonify({'message': 'Connected', **result}), 200
823
+
824
+
725
825
  @bp.route('/connections/<name>', methods=['DELETE'])
726
826
  def delete_connection(name):
827
+ """Deleting a connection is destructive and hard to reason about after the fact - every saved query that
828
+ used it stops working - so the caller must say why. The reason is stored nowhere but the audit log (there
829
+ is no separate "deleted connections" table); the admin UI's own Deleted tab reads it straight from there,
830
+ the same as everything else on this route already does."""
727
831
  require_admin()
728
832
  before = store.read_connections().get(name)
833
+ if before is None:
834
+ raise ApiError(f"Connection '{name}' not found", 404)
835
+ data = get_json_body(required=False)
836
+ reason = (data.get('reason') or '').strip()
837
+ if not reason:
838
+ raise ApiError('A reason is required to delete a connection')
729
839
  store.delete_connection(name)
730
- if before is not None:
731
- store.record_audit(caller_key_name(), 'delete_connection', name, store.mask_passwords({name: before})[name])
840
+ changes = {**store.mask_passwords({name: before})[name], 'deleted_reason': reason}
841
+ store.record_audit(caller_key_name(), 'delete_connection', name, changes)
732
842
  pool.close_pooled_connections() # a removed connection must not keep serving from idle sockets
733
843
  return jsonify({'message': f"Connection '{name}' deleted"}), 200
734
844
 
@@ -736,7 +846,10 @@ def delete_connection(name):
736
846
  @bp.route('/connections/<name>/schema', methods=['GET'])
737
847
  def connection_schema(name):
738
848
  require_connection(name)
739
- return jsonify(schema.fetch_schema(name)), 200
849
+ database = request.args.get('database') or None
850
+ if database and not g.permission.admin:
851
+ raise ApiError('Only the admin key may browse a different database on this connection', 403)
852
+ return jsonify(schema.fetch_schema(name, database=database)), 200
740
853
 
741
854
 
742
855
  # --------------------------------------------------------------------------------------
@@ -846,6 +959,16 @@ def audit_log_endpoint():
846
959
  return jsonify({'entries': list(reversed(store.read_audit_log()))}), 200
847
960
 
848
961
 
962
+ @bp.route('/settings', methods=['GET'])
963
+ def settings_endpoint():
964
+ """The server's own configuration as the admin UI's Settings screen shows it: every environment variable
965
+ that matters, its effective value and whether it was set or is the default. Read-only, and admin only
966
+ like the rest of what reveals the server's configuration. Secrets are reported as configured or not,
967
+ never returned."""
968
+ require_admin()
969
+ return jsonify({'sections': config.describe_settings()}), 200
970
+
971
+
849
972
  # --------------------------------------------------------------------------------------
850
973
  # Service endpoints
851
974
  # --------------------------------------------------------------------------------------
@@ -871,6 +994,14 @@ def metrics_endpoint():
871
994
  return Response(metrics.render(), mimetype='text/plain; version=0.0.4; charset=utf-8')
872
995
 
873
996
 
997
+ def _is_runnable(data):
998
+ """Whether a saved-query version has something to actually execute - a SQL string, or (query_type
999
+ 'mongo') a Mongo collection - so a definition left mid-edit or otherwise malformed never gets listed
1000
+ somewhere a caller might then try to run it."""
1001
+ return isinstance(data.get('sql_query'), str) or (data.get('query_type') == 'mongo'
1002
+ and isinstance(data.get('mongo_collection'), str))
1003
+
1004
+
874
1005
  def describe_saved_queries(permission):
875
1006
  """What the OpenAPI document needs to know about each saved query (never its SQL text) that
876
1007
  ``permission`` may actually call - the same test run_saved() applies, so a key scoped to specific
@@ -878,8 +1009,7 @@ def describe_saved_queries(permission):
878
1009
  unrestricted (admin, or connection-wide) key sees everything, unchanged from before this filter."""
879
1010
  described = []
880
1011
  for name, number, data, collection in store.latest_versions():
881
- sql = data.get('sql_query')
882
- if not isinstance(sql, str):
1012
+ if not _is_runnable(data):
883
1013
  continue
884
1014
  connection_name = data.get('connection_name')
885
1015
  if not apikeys.can_run_saved(permission, name, collection, connection_name):
@@ -910,8 +1040,7 @@ def describe_catalog(permission):
910
1040
  caller* can write through it (apikeys.can_write_query() - independent of their blanket allow_writes)."""
911
1041
  catalog = []
912
1042
  for name, number, data, collection in store.latest_versions():
913
- sql = data.get('sql_query')
914
- if not isinstance(sql, str):
1043
+ if not _is_runnable(data):
915
1044
  continue
916
1045
  connection_name = data.get('connection_name')
917
1046
  if not apikeys.can_run_saved(permission, name, collection, connection_name):
@@ -18,7 +18,8 @@ FORMAT_VERSION = 1
18
18
  POLICIES = ('fail', 'skip', 'new-version')
19
19
  # Everything an entry may carry. Anything else is an error rather than silently dropped: a field the importer
20
20
  # ignores is a field whose loss nobody notices.
21
- ENTRY_FIELDS = {'name', 'sql_query', 'author', 'description', 'tags', 'query_parameters', 'connection_name',
21
+ ENTRY_FIELDS = {'name', 'sql_query', 'query_type', 'mongo_collection', 'mongo_filter', 'mongo_projection',
22
+ 'mongo_sort', 'author', 'description', 'tags', 'query_parameters', 'connection_name',
22
23
  'cache_ttl'}
23
24
 
24
25
 
@@ -3,7 +3,7 @@ import os
3
3
  import re
4
4
  from pathlib import Path
5
5
 
6
- SUPPORTED_DB_TYPES = ('mysql', 'postgres', 'clickhouse', 'sqlite', 'h2', 'jdbc', 'duckdb')
6
+ SUPPORTED_DB_TYPES = ('mysql', 'postgres', 'clickhouse', 'sqlite', 'h2', 'jdbc', 'duckdb', 'mongo')
7
7
  PASSWORD_MASK = '********'
8
8
  CONNECT_TIMEOUT = 10 # seconds
9
9
  HISTORY_LIMIT = 50 # executions remembered per saved-query version
@@ -278,3 +278,80 @@ def slow_query_threshold():
278
278
  if value == 0:
279
279
  return None
280
280
  return value if value > 0 else DEFAULT_SLOW_QUERY_THRESHOLD
281
+
282
+
283
+ def _seconds(value):
284
+ return None if value is None else f'{value:g} s'
285
+
286
+
287
+ def describe_settings():
288
+ """Every server setting the admin UI's Settings screen shows, grouped: [{id, title, description, rows}].
289
+
290
+ Each row is {label, description, env, value, source, env_value}. ``value`` is what to display (secrets are
291
+ reduced to "configured" / "enabled" - never the secret itself), ``source`` is "env" when the variable is set
292
+ and "default" when the built-in default applies, and ``env_value`` is the raw value for a "copy as .env"
293
+ export, or None for a secret so that export can never leak one. Read-only: settings are environment
294
+ variables, changed by restarting the server, not through the API."""
295
+ def row(label, description, env, value, secret=False):
296
+ raw = os.environ.get(env, '').strip()
297
+ return {'label': label, 'description': description, 'env': env, 'value': value,
298
+ 'source': 'env' if raw else 'default', 'env_value': None if secret or not raw else raw}
299
+
300
+ def on_off(flag):
301
+ return 'on' if flag else 'off'
302
+
303
+ stream = stream_max_rows()
304
+ timeout, slow = query_timeout(), slow_query_threshold()
305
+ limit, export = rate_limit(), audit_log_export_file()
306
+ cors = cors_origins()
307
+ return [
308
+ {'id': 'general', 'title': 'General',
309
+ 'description': 'Where this server keeps its files and what it loads at startup.', 'rows': [
310
+ row('Home directory', 'Folder holding db_connections.json, api_keys.json, roles.json and saved_sql/.',
311
+ 'QUERYAPIGATE_HOME', str(home())),
312
+ row('Load examples', 'Load the example APIs at startup. Idempotent.',
313
+ 'QUERYAPIGATE_LOAD_EXAMPLES', on_off(load_examples())),
314
+ row('H2 driver', 'JAR used for h2 connections.', 'QUERYAPIGATE_H2_JAR',
315
+ Path(h2_jar()).name + ('' if os.environ.get('QUERYAPIGATE_H2_JAR') else ' (bundled)'))]},
316
+ {'id': 'security', 'title': 'Security',
317
+ 'description': 'Admin access, write protection and secrets at rest.', 'rows': [
318
+ row('Admin API key', 'Key with full access to this UI and the admin API. Unset means open access.',
319
+ 'QUERYAPIGATE_API_KEY', 'configured' if api_key() else 'not set - open access', secret=True),
320
+ row('Allow writes', 'When off, statements that modify data are rejected.',
321
+ 'QUERYAPIGATE_ALLOW_WRITES', on_off(allow_writes())),
322
+ row('Encryption at rest', 'Fernet key used to encrypt connection passwords on disk.',
323
+ 'QUERYAPIGATE_SECRET_KEY', 'enabled' if secret_key() else 'off', secret=True),
324
+ row('Trusted proxy hops', 'Reverse proxies whose X-Forwarded-* headers are trusted.',
325
+ 'QUERYAPIGATE_TRUST_PROXY', str(proxy_hops()))]},
326
+ {'id': 'execution', 'title': 'Query execution',
327
+ 'description': 'Limits applied to every statement this server runs.', 'rows': [
328
+ row('Query timeout', 'Server-wide statement limit. A request may ask for less, never more. '
329
+ '0 disables it.', 'QUERYAPIGATE_QUERY_TIMEOUT', _seconds(timeout) or 'off'),
330
+ row('Max page size', 'Largest page_size a caller may request.', 'QUERYAPIGATE_MAX_PAGE_SIZE',
331
+ f'{max_page_size()} rows'),
332
+ row('Stream row cap', 'Row cap for ?stream=true exports.', 'QUERYAPIGATE_STREAM_MAX_ROWS',
333
+ 'unbounded' if stream is None else f'{stream} rows'),
334
+ row('Slow query threshold', 'Queries slower than this are logged as warnings. 0 disables it.',
335
+ 'QUERYAPIGATE_SLOW_QUERY_THRESHOLD', _seconds(slow) or 'off')]},
336
+ {'id': 'pool', 'title': 'Connection pool',
337
+ 'description': 'Idle connections kept open per distinct connection.', 'rows': [
338
+ row('Pool size', 'Idle connections kept per connection. 0 disables pooling.',
339
+ 'QUERYAPIGATE_POOL_SIZE', str(pool_size())),
340
+ row('Idle timeout', 'How long an idle pooled connection is kept before it is closed.',
341
+ 'QUERYAPIGATE_POOL_IDLE_TIMEOUT', _seconds(pool_idle_timeout()))]},
342
+ {'id': 'traffic', 'title': 'Rate limits & CORS',
343
+ 'description': 'Per-client throttling and browser origins allowed to call the API.', 'rows': [
344
+ row('Rate limit', 'Requests allowed per client. API keys and roles can set their own.',
345
+ 'QUERYAPIGATE_RATE_LIMIT', format_rate_limit(limit) or 'off'),
346
+ row('CORS origins', 'Comma-separated origins, or * for any. Unset turns CORS off.',
347
+ 'QUERYAPIGATE_CORS_ORIGINS',
348
+ 'off' if cors is None else '*' if cors == '*' else ', '.join(sorted(cors)))]},
349
+ {'id': 'audit', 'title': 'Audit & logging',
350
+ 'description': 'Retention of administrative actions and log format.', 'rows': [
351
+ row('Audit log limit', 'Entries kept in audit_log.json.', 'QUERYAPIGATE_AUDIT_LOG_LIMIT',
352
+ str(audit_log_limit())),
353
+ row('Audit export file', 'Append-only JSON-lines copy of every audit entry, never rolled off.',
354
+ 'QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE', 'not set' if export is None else str(export)),
355
+ row('JSON logs', 'One JSON object per line instead of plain text.', 'QUERYAPIGATE_JSON_LOGS',
356
+ on_off(json_logs()))]},
357
+ ]