api-dock 0.2.2__tar.gz → 0.3.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.
- {api_dock-0.2.2 → api_dock-0.3.0}/PKG-INFO +213 -1
- {api_dock-0.2.2 → api_dock-0.3.0}/README.md +212 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/config.py +61 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/database_config.py +110 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/fast_api.py +2 -1
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/flask_api.py +4 -2
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/route_mapper.py +31 -7
- api_dock-0.3.0/api_dock/sql_builder.py +628 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/PKG-INFO +213 -1
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/SOURCES.txt +3 -1
- api_dock-0.3.0/config/databases/test_users.yaml +127 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/pyproject.toml +2 -1
- api_dock-0.3.0/tests/test_sql_append.py +451 -0
- api_dock-0.2.2/api_dock/sql_builder.py +0 -163
- {api_dock-0.2.2 → api_dock-0.3.0}/LICENSE.md +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/__init__.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/cli.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/config_discovery.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/storage_auth.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/dependency_links.txt +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/entry_points.txt +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/requires.txt +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/top_level.txt +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/config.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/databases/db_example.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_allowed_routes.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_custom_mapping.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_restrictions.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_wildcards.yaml +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/setup.cfg +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_config_syntax.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_curl_fixes.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_restrictions.py +0 -0
- {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_root_endpoint.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api_dock
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A flexible API gateway that allows you to proxy requests to multiple remote APIs and Databases
|
|
5
5
|
Author-email: Brookie Guzder-Williams <bguzder-williams@berkeley.edu>
|
|
6
6
|
License: BSd 3-clause
|
|
@@ -44,6 +44,7 @@ API Dock (API(s) + (data)Base(s)/base-(for)-API(s)) a flexible API gateway that
|
|
|
44
44
|
- [Main Configuration](#main-configuration-api_dock_configconfigyaml)
|
|
45
45
|
- [Remote Configurations](#remote-configurations)
|
|
46
46
|
- [SQL Database Support](#sql-database-support)
|
|
47
|
+
- [URL Query Parameters](#url-query-parameters)
|
|
47
48
|
- [Using RouteMapper in Your Own Projects](#using-routemapper-in-your-own-projects)
|
|
48
49
|
- [Basic Integration](#basic-integration)
|
|
49
50
|
- [Framework Examples](#framework-examples)
|
|
@@ -59,6 +60,7 @@ API Dock (API(s) + (data)Base(s)/base-(for)-API(s)) a flexible API gateway that
|
|
|
59
60
|
- **YAML Configuration**: Simple, human-readable configuration files
|
|
60
61
|
- **Access Control**: Define allowed/restricted routes per remote API
|
|
61
62
|
- **Version Support**: Handle API versioning in URL paths
|
|
63
|
+
- **URL Query Parameters**: Declarative query parameter support for database routes with filtering, sorting, pagination, conditional logic, and direct responses
|
|
62
64
|
- **Flexibility**: Quickly launch FastAPI or Flask apps, or easily integrate into any existing framework
|
|
63
65
|
|
|
64
66
|
## Install
|
|
@@ -477,6 +479,216 @@ routes:
|
|
|
477
479
|
|
|
478
480
|
---
|
|
479
481
|
|
|
482
|
+
## URL Query Parameters
|
|
483
|
+
|
|
484
|
+
Database routes support declarative URL query parameters via the `query_params` section. This lets you add filtering, sorting, pagination, conditional logic, and direct responses — all driven by the URL query string.
|
|
485
|
+
|
|
486
|
+
Routes without a `query_params` section work exactly as before (full backward compatibility).
|
|
487
|
+
|
|
488
|
+
### Basic Filtering with `sql`
|
|
489
|
+
|
|
490
|
+
Use `sql` to add WHERE clause fragments. Each fragment is joined with `AND`. Optional by default — only included if the parameter is in the URL.
|
|
491
|
+
|
|
492
|
+
```yaml
|
|
493
|
+
routes:
|
|
494
|
+
- route: users
|
|
495
|
+
sql: SELECT * FROM [[users]]
|
|
496
|
+
query_params:
|
|
497
|
+
- age:
|
|
498
|
+
sql: age = {{age}} # optional — only if ?age= provided
|
|
499
|
+
- department:
|
|
500
|
+
sql: department = '{{department}}'
|
|
501
|
+
- height:
|
|
502
|
+
sql: height < {{height}}
|
|
503
|
+
default: 200 # always included (uses 200 if not in URL)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
GET /db/users?age=25&department=engineering
|
|
508
|
+
# SQL: SELECT * FROM users WHERE age = 25 AND height < 200 AND department = 'engineering'
|
|
509
|
+
|
|
510
|
+
GET /db/users
|
|
511
|
+
# SQL: SELECT * FROM users WHERE height < 200
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Sorting and Pagination with `sql_append`
|
|
515
|
+
|
|
516
|
+
Use `sql_append` to append clauses *after* the WHERE clause — for `ORDER BY`, `LIMIT`, `OFFSET`, etc. Fragments are appended in the order they appear in the YAML config, so **the YAML order must match valid SQL order** (ORDER BY before LIMIT before OFFSET).
|
|
517
|
+
|
|
518
|
+
`sql_append` templates can reference `{{variables}}` from other parameters, including **value-only parameters** — params that only have a `default` and exist solely to provide a variable for other templates.
|
|
519
|
+
|
|
520
|
+
```yaml
|
|
521
|
+
routes:
|
|
522
|
+
- route: users
|
|
523
|
+
sql: SELECT * FROM [[users]]
|
|
524
|
+
query_params:
|
|
525
|
+
# WHERE clause params
|
|
526
|
+
- department:
|
|
527
|
+
sql: department = '{{department}}'
|
|
528
|
+
# Post-WHERE params
|
|
529
|
+
- sort:
|
|
530
|
+
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
531
|
+
default: created_date
|
|
532
|
+
- sort_direction:
|
|
533
|
+
default: DESC # value-only param — feeds into sort's template
|
|
534
|
+
- limit:
|
|
535
|
+
sql_append: LIMIT {{limit}}
|
|
536
|
+
default: 50
|
|
537
|
+
- offset:
|
|
538
|
+
sql_append: OFFSET {{offset}} # optional — only if ?offset= provided
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
GET /db/users?department=engineering&sort=name&sort_direction=ASC&limit=10
|
|
543
|
+
# SQL: SELECT * FROM users WHERE department = 'engineering' ORDER BY name ASC LIMIT 10
|
|
544
|
+
|
|
545
|
+
GET /db/users
|
|
546
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 50
|
|
547
|
+
|
|
548
|
+
GET /db/users?limit=20&offset=40
|
|
549
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 20 OFFSET 40
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Required Parameters
|
|
553
|
+
|
|
554
|
+
Use `required: true` to return a `400` error if the parameter is missing. Optionally provide a custom error response with `missing_response`.
|
|
555
|
+
|
|
556
|
+
```yaml
|
|
557
|
+
query_params:
|
|
558
|
+
- report_type:
|
|
559
|
+
sql: report_type = {{report_type}}
|
|
560
|
+
required: true
|
|
561
|
+
missing_response:
|
|
562
|
+
error: "report_type is required"
|
|
563
|
+
valid_types: ["summary", "detailed"]
|
|
564
|
+
http_status: 400
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
GET /db/reports
|
|
569
|
+
# Response (400): {"error": "report_type is required", "valid_types": [...], "http_status": 400}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Direct Responses with `response`
|
|
573
|
+
|
|
574
|
+
Use `response` to return a fixed JSON or string response immediately when the parameter is present (no SQL is executed).
|
|
575
|
+
|
|
576
|
+
```yaml
|
|
577
|
+
query_params:
|
|
578
|
+
- debug:
|
|
579
|
+
response:
|
|
580
|
+
message: Debug mode enabled
|
|
581
|
+
info: "This endpoint queries the users table"
|
|
582
|
+
- sleeping:
|
|
583
|
+
response: "Wake up! This endpoint is disabled during sleep mode."
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
GET /db/users?debug=anything
|
|
588
|
+
# Response (200): {"message": "Debug mode enabled", "info": "This endpoint queries the users table"}
|
|
589
|
+
|
|
590
|
+
GET /db/users?sleeping=true
|
|
591
|
+
# Response (200): "Wake up! This endpoint is disabled during sleep mode."
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Conditional Logic with `conditional`
|
|
595
|
+
|
|
596
|
+
Use `conditional` to branch on the parameter's value. Each branch can lead to a `sql` fragment, a `response`, or an `action`.
|
|
597
|
+
|
|
598
|
+
```yaml
|
|
599
|
+
query_params:
|
|
600
|
+
- enrolled:
|
|
601
|
+
conditional:
|
|
602
|
+
true:
|
|
603
|
+
sql: enrolled = true # adds to WHERE clause
|
|
604
|
+
false:
|
|
605
|
+
sql: enrolled = false
|
|
606
|
+
pending:
|
|
607
|
+
response:
|
|
608
|
+
message: "Pending users cannot be queried"
|
|
609
|
+
action: "Contact admin"
|
|
610
|
+
default:
|
|
611
|
+
response: "Unknown enrollment status"
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
GET /db/users?enrolled=true
|
|
616
|
+
# SQL: SELECT * FROM users WHERE enrolled = true
|
|
617
|
+
|
|
618
|
+
GET /db/users?enrolled=pending
|
|
619
|
+
# Response (200): {"message": "Pending users cannot be queried", "action": "Contact admin"}
|
|
620
|
+
|
|
621
|
+
GET /db/users?enrolled=xyz
|
|
622
|
+
# Response (200): "Unknown enrollment status"
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Complete Example
|
|
626
|
+
|
|
627
|
+
Combining all parameter types in a single route:
|
|
628
|
+
|
|
629
|
+
```yaml
|
|
630
|
+
name: my_database
|
|
631
|
+
tables:
|
|
632
|
+
users: s3://bucket/users.parquet
|
|
633
|
+
|
|
634
|
+
routes:
|
|
635
|
+
- route: users/search
|
|
636
|
+
sql: SELECT * FROM [[users]]
|
|
637
|
+
query_params:
|
|
638
|
+
# WHERE clause filters
|
|
639
|
+
- name:
|
|
640
|
+
sql: name ILIKE '%{{name}}%'
|
|
641
|
+
- age_min:
|
|
642
|
+
sql: age >= {{age_min}}
|
|
643
|
+
- age_max:
|
|
644
|
+
sql: age <= {{age_max}}
|
|
645
|
+
- department:
|
|
646
|
+
sql: department = '{{department}}'
|
|
647
|
+
# Sorting and pagination (sql_append)
|
|
648
|
+
- sort:
|
|
649
|
+
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
650
|
+
default: created_date
|
|
651
|
+
- sort_direction:
|
|
652
|
+
default: DESC
|
|
653
|
+
- limit:
|
|
654
|
+
sql_append: LIMIT {{limit}}
|
|
655
|
+
default: 50
|
|
656
|
+
- offset:
|
|
657
|
+
sql_append: OFFSET {{offset}}
|
|
658
|
+
# Direct response
|
|
659
|
+
- sleeping:
|
|
660
|
+
response: "Search is disabled during sleep mode."
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
# Full search with filters, sorting, and pagination
|
|
665
|
+
GET /my_database/users/search?name=john&age_min=21&age_max=65&sort=age&sort_direction=ASC&limit=20&offset=40
|
|
666
|
+
# SQL: SELECT * FROM users
|
|
667
|
+
# WHERE name ILIKE '%john%' AND age >= 21 AND age <= 65
|
|
668
|
+
# ORDER BY age ASC LIMIT 20 OFFSET 40
|
|
669
|
+
|
|
670
|
+
# Just defaults
|
|
671
|
+
GET /my_database/users/search
|
|
672
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 50
|
|
673
|
+
|
|
674
|
+
# Direct response, no SQL
|
|
675
|
+
GET /my_database/users/search?sleeping=true
|
|
676
|
+
# Response: "Search is disabled during sleep mode."
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Processing Order
|
|
680
|
+
|
|
681
|
+
Parameters are processed in this order (first match wins for early returns):
|
|
682
|
+
|
|
683
|
+
1. `response` parameters — return immediately if parameter present
|
|
684
|
+
2. `conditional` parameters — evaluate value, may return response or add SQL
|
|
685
|
+
3. `required` parameters — return 400 if missing
|
|
686
|
+
4. `sql` parameters — build WHERE clause fragments
|
|
687
|
+
5. `sql_append` parameters — append post-WHERE clauses (ORDER BY, LIMIT, etc.)
|
|
688
|
+
6. Execute final SQL query
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
480
692
|
# Using RouteMapper in Your Own Projects
|
|
481
693
|
|
|
482
694
|
The core functionality is available as a standalone `RouteMapper` class that can be integrated into any web framework:
|
|
@@ -14,6 +14,7 @@ API Dock (API(s) + (data)Base(s)/base-(for)-API(s)) a flexible API gateway that
|
|
|
14
14
|
- [Main Configuration](#main-configuration-api_dock_configconfigyaml)
|
|
15
15
|
- [Remote Configurations](#remote-configurations)
|
|
16
16
|
- [SQL Database Support](#sql-database-support)
|
|
17
|
+
- [URL Query Parameters](#url-query-parameters)
|
|
17
18
|
- [Using RouteMapper in Your Own Projects](#using-routemapper-in-your-own-projects)
|
|
18
19
|
- [Basic Integration](#basic-integration)
|
|
19
20
|
- [Framework Examples](#framework-examples)
|
|
@@ -29,6 +30,7 @@ API Dock (API(s) + (data)Base(s)/base-(for)-API(s)) a flexible API gateway that
|
|
|
29
30
|
- **YAML Configuration**: Simple, human-readable configuration files
|
|
30
31
|
- **Access Control**: Define allowed/restricted routes per remote API
|
|
31
32
|
- **Version Support**: Handle API versioning in URL paths
|
|
33
|
+
- **URL Query Parameters**: Declarative query parameter support for database routes with filtering, sorting, pagination, conditional logic, and direct responses
|
|
32
34
|
- **Flexibility**: Quickly launch FastAPI or Flask apps, or easily integrate into any existing framework
|
|
33
35
|
|
|
34
36
|
## Install
|
|
@@ -447,6 +449,216 @@ routes:
|
|
|
447
449
|
|
|
448
450
|
---
|
|
449
451
|
|
|
452
|
+
## URL Query Parameters
|
|
453
|
+
|
|
454
|
+
Database routes support declarative URL query parameters via the `query_params` section. This lets you add filtering, sorting, pagination, conditional logic, and direct responses — all driven by the URL query string.
|
|
455
|
+
|
|
456
|
+
Routes without a `query_params` section work exactly as before (full backward compatibility).
|
|
457
|
+
|
|
458
|
+
### Basic Filtering with `sql`
|
|
459
|
+
|
|
460
|
+
Use `sql` to add WHERE clause fragments. Each fragment is joined with `AND`. Optional by default — only included if the parameter is in the URL.
|
|
461
|
+
|
|
462
|
+
```yaml
|
|
463
|
+
routes:
|
|
464
|
+
- route: users
|
|
465
|
+
sql: SELECT * FROM [[users]]
|
|
466
|
+
query_params:
|
|
467
|
+
- age:
|
|
468
|
+
sql: age = {{age}} # optional — only if ?age= provided
|
|
469
|
+
- department:
|
|
470
|
+
sql: department = '{{department}}'
|
|
471
|
+
- height:
|
|
472
|
+
sql: height < {{height}}
|
|
473
|
+
default: 200 # always included (uses 200 if not in URL)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
GET /db/users?age=25&department=engineering
|
|
478
|
+
# SQL: SELECT * FROM users WHERE age = 25 AND height < 200 AND department = 'engineering'
|
|
479
|
+
|
|
480
|
+
GET /db/users
|
|
481
|
+
# SQL: SELECT * FROM users WHERE height < 200
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Sorting and Pagination with `sql_append`
|
|
485
|
+
|
|
486
|
+
Use `sql_append` to append clauses *after* the WHERE clause — for `ORDER BY`, `LIMIT`, `OFFSET`, etc. Fragments are appended in the order they appear in the YAML config, so **the YAML order must match valid SQL order** (ORDER BY before LIMIT before OFFSET).
|
|
487
|
+
|
|
488
|
+
`sql_append` templates can reference `{{variables}}` from other parameters, including **value-only parameters** — params that only have a `default` and exist solely to provide a variable for other templates.
|
|
489
|
+
|
|
490
|
+
```yaml
|
|
491
|
+
routes:
|
|
492
|
+
- route: users
|
|
493
|
+
sql: SELECT * FROM [[users]]
|
|
494
|
+
query_params:
|
|
495
|
+
# WHERE clause params
|
|
496
|
+
- department:
|
|
497
|
+
sql: department = '{{department}}'
|
|
498
|
+
# Post-WHERE params
|
|
499
|
+
- sort:
|
|
500
|
+
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
501
|
+
default: created_date
|
|
502
|
+
- sort_direction:
|
|
503
|
+
default: DESC # value-only param — feeds into sort's template
|
|
504
|
+
- limit:
|
|
505
|
+
sql_append: LIMIT {{limit}}
|
|
506
|
+
default: 50
|
|
507
|
+
- offset:
|
|
508
|
+
sql_append: OFFSET {{offset}} # optional — only if ?offset= provided
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
GET /db/users?department=engineering&sort=name&sort_direction=ASC&limit=10
|
|
513
|
+
# SQL: SELECT * FROM users WHERE department = 'engineering' ORDER BY name ASC LIMIT 10
|
|
514
|
+
|
|
515
|
+
GET /db/users
|
|
516
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 50
|
|
517
|
+
|
|
518
|
+
GET /db/users?limit=20&offset=40
|
|
519
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 20 OFFSET 40
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Required Parameters
|
|
523
|
+
|
|
524
|
+
Use `required: true` to return a `400` error if the parameter is missing. Optionally provide a custom error response with `missing_response`.
|
|
525
|
+
|
|
526
|
+
```yaml
|
|
527
|
+
query_params:
|
|
528
|
+
- report_type:
|
|
529
|
+
sql: report_type = {{report_type}}
|
|
530
|
+
required: true
|
|
531
|
+
missing_response:
|
|
532
|
+
error: "report_type is required"
|
|
533
|
+
valid_types: ["summary", "detailed"]
|
|
534
|
+
http_status: 400
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
GET /db/reports
|
|
539
|
+
# Response (400): {"error": "report_type is required", "valid_types": [...], "http_status": 400}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Direct Responses with `response`
|
|
543
|
+
|
|
544
|
+
Use `response` to return a fixed JSON or string response immediately when the parameter is present (no SQL is executed).
|
|
545
|
+
|
|
546
|
+
```yaml
|
|
547
|
+
query_params:
|
|
548
|
+
- debug:
|
|
549
|
+
response:
|
|
550
|
+
message: Debug mode enabled
|
|
551
|
+
info: "This endpoint queries the users table"
|
|
552
|
+
- sleeping:
|
|
553
|
+
response: "Wake up! This endpoint is disabled during sleep mode."
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
GET /db/users?debug=anything
|
|
558
|
+
# Response (200): {"message": "Debug mode enabled", "info": "This endpoint queries the users table"}
|
|
559
|
+
|
|
560
|
+
GET /db/users?sleeping=true
|
|
561
|
+
# Response (200): "Wake up! This endpoint is disabled during sleep mode."
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Conditional Logic with `conditional`
|
|
565
|
+
|
|
566
|
+
Use `conditional` to branch on the parameter's value. Each branch can lead to a `sql` fragment, a `response`, or an `action`.
|
|
567
|
+
|
|
568
|
+
```yaml
|
|
569
|
+
query_params:
|
|
570
|
+
- enrolled:
|
|
571
|
+
conditional:
|
|
572
|
+
true:
|
|
573
|
+
sql: enrolled = true # adds to WHERE clause
|
|
574
|
+
false:
|
|
575
|
+
sql: enrolled = false
|
|
576
|
+
pending:
|
|
577
|
+
response:
|
|
578
|
+
message: "Pending users cannot be queried"
|
|
579
|
+
action: "Contact admin"
|
|
580
|
+
default:
|
|
581
|
+
response: "Unknown enrollment status"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
GET /db/users?enrolled=true
|
|
586
|
+
# SQL: SELECT * FROM users WHERE enrolled = true
|
|
587
|
+
|
|
588
|
+
GET /db/users?enrolled=pending
|
|
589
|
+
# Response (200): {"message": "Pending users cannot be queried", "action": "Contact admin"}
|
|
590
|
+
|
|
591
|
+
GET /db/users?enrolled=xyz
|
|
592
|
+
# Response (200): "Unknown enrollment status"
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Complete Example
|
|
596
|
+
|
|
597
|
+
Combining all parameter types in a single route:
|
|
598
|
+
|
|
599
|
+
```yaml
|
|
600
|
+
name: my_database
|
|
601
|
+
tables:
|
|
602
|
+
users: s3://bucket/users.parquet
|
|
603
|
+
|
|
604
|
+
routes:
|
|
605
|
+
- route: users/search
|
|
606
|
+
sql: SELECT * FROM [[users]]
|
|
607
|
+
query_params:
|
|
608
|
+
# WHERE clause filters
|
|
609
|
+
- name:
|
|
610
|
+
sql: name ILIKE '%{{name}}%'
|
|
611
|
+
- age_min:
|
|
612
|
+
sql: age >= {{age_min}}
|
|
613
|
+
- age_max:
|
|
614
|
+
sql: age <= {{age_max}}
|
|
615
|
+
- department:
|
|
616
|
+
sql: department = '{{department}}'
|
|
617
|
+
# Sorting and pagination (sql_append)
|
|
618
|
+
- sort:
|
|
619
|
+
sql_append: ORDER BY {{sort}} {{sort_direction}}
|
|
620
|
+
default: created_date
|
|
621
|
+
- sort_direction:
|
|
622
|
+
default: DESC
|
|
623
|
+
- limit:
|
|
624
|
+
sql_append: LIMIT {{limit}}
|
|
625
|
+
default: 50
|
|
626
|
+
- offset:
|
|
627
|
+
sql_append: OFFSET {{offset}}
|
|
628
|
+
# Direct response
|
|
629
|
+
- sleeping:
|
|
630
|
+
response: "Search is disabled during sleep mode."
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
# Full search with filters, sorting, and pagination
|
|
635
|
+
GET /my_database/users/search?name=john&age_min=21&age_max=65&sort=age&sort_direction=ASC&limit=20&offset=40
|
|
636
|
+
# SQL: SELECT * FROM users
|
|
637
|
+
# WHERE name ILIKE '%john%' AND age >= 21 AND age <= 65
|
|
638
|
+
# ORDER BY age ASC LIMIT 20 OFFSET 40
|
|
639
|
+
|
|
640
|
+
# Just defaults
|
|
641
|
+
GET /my_database/users/search
|
|
642
|
+
# SQL: SELECT * FROM users ORDER BY created_date DESC LIMIT 50
|
|
643
|
+
|
|
644
|
+
# Direct response, no SQL
|
|
645
|
+
GET /my_database/users/search?sleeping=true
|
|
646
|
+
# Response: "Search is disabled during sleep mode."
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Processing Order
|
|
650
|
+
|
|
651
|
+
Parameters are processed in this order (first match wins for early returns):
|
|
652
|
+
|
|
653
|
+
1. `response` parameters — return immediately if parameter present
|
|
654
|
+
2. `conditional` parameters — evaluate value, may return response or add SQL
|
|
655
|
+
3. `required` parameters — return 400 if missing
|
|
656
|
+
4. `sql` parameters — build WHERE clause fragments
|
|
657
|
+
5. `sql_append` parameters — append post-WHERE clauses (ORDER BY, LIMIT, etc.)
|
|
658
|
+
6. Execute final SQL query
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
450
662
|
# Using RouteMapper in Your Own Projects
|
|
451
663
|
|
|
452
664
|
The core functionality is available as a standalone `RouteMapper` class that can be integrated into any web framework:
|
|
@@ -390,6 +390,67 @@ def find_route_mapping(full_route: str, method: str, remote_config: Dict[str, An
|
|
|
390
390
|
return None
|
|
391
391
|
|
|
392
392
|
|
|
393
|
+
def filter_remote_query_params(
|
|
394
|
+
query_params: Dict[str, str],
|
|
395
|
+
route: str,
|
|
396
|
+
method: str,
|
|
397
|
+
remote_config: Dict[str, Any]
|
|
398
|
+
) -> Dict[str, str]:
|
|
399
|
+
"""Filter query parameters based on remote config settings.
|
|
400
|
+
|
|
401
|
+
Checks for a route-level query_params setting first, then falls back
|
|
402
|
+
to the top-level remote config query_params. If neither exists, all
|
|
403
|
+
params are passed through unchanged (backward compatible).
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
query_params: Original query parameters from the request.
|
|
407
|
+
route: The actual route path (e.g., "users/123").
|
|
408
|
+
method: HTTP method (e.g., "GET", "POST").
|
|
409
|
+
remote_config: Remote configuration dictionary.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Filtered query parameters dictionary.
|
|
413
|
+
"""
|
|
414
|
+
routes = remote_config.get("routes", [])
|
|
415
|
+
|
|
416
|
+
# Find matching route and its query_params setting
|
|
417
|
+
route_query_params_setting = None
|
|
418
|
+
for route_entry in routes:
|
|
419
|
+
if isinstance(route_entry, str):
|
|
420
|
+
if _route_matches_pattern(route, route_entry):
|
|
421
|
+
# String routes have no query_params setting
|
|
422
|
+
break
|
|
423
|
+
elif isinstance(route_entry, dict):
|
|
424
|
+
if _route_matches_pattern(route, route_entry, method):
|
|
425
|
+
route_query_params_setting = route_entry.get("query_params")
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
# Fall back to top-level setting if route has no query_params key
|
|
429
|
+
if route_query_params_setting is None:
|
|
430
|
+
setting = remote_config.get("query_params")
|
|
431
|
+
else:
|
|
432
|
+
setting = route_query_params_setting
|
|
433
|
+
|
|
434
|
+
# No setting anywhere → pass all params through (backward compatible)
|
|
435
|
+
if setting is None:
|
|
436
|
+
return query_params
|
|
437
|
+
|
|
438
|
+
# True → all params
|
|
439
|
+
if setting is True:
|
|
440
|
+
return query_params
|
|
441
|
+
|
|
442
|
+
# False → no params
|
|
443
|
+
if setting is False:
|
|
444
|
+
return {}
|
|
445
|
+
|
|
446
|
+
# List → whitelist filter
|
|
447
|
+
if isinstance(setting, list):
|
|
448
|
+
return {k: v for k, v in query_params.items() if k in setting}
|
|
449
|
+
|
|
450
|
+
# Unknown type → pass all through
|
|
451
|
+
return query_params
|
|
452
|
+
|
|
453
|
+
|
|
393
454
|
#
|
|
394
455
|
# INTERNAL
|
|
395
456
|
#
|
|
@@ -132,6 +132,45 @@ def get_table_metadata(table_name: str, database_config: Dict[str, Any]) -> Dict
|
|
|
132
132
|
return {}
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def merge_query_params(route_config: Dict[str, Any], database_config: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
|
+
"""Merge top-level query_params into route config.
|
|
137
|
+
|
|
138
|
+
Top-level query_params from the database config are applied to every route.
|
|
139
|
+
Route-level params take precedence: if a route defines a param with the same
|
|
140
|
+
name as a top-level param, the route version wins. Non-overridden top-level
|
|
141
|
+
params are appended after route params.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
route_config: Route configuration dictionary.
|
|
145
|
+
database_config: Database configuration dictionary (may contain top-level query_params).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Route config dict with merged query_params. Returns original route_config
|
|
149
|
+
if no top-level query_params exist.
|
|
150
|
+
"""
|
|
151
|
+
top_level = database_config.get("query_params", [])
|
|
152
|
+
if not top_level:
|
|
153
|
+
return route_config
|
|
154
|
+
|
|
155
|
+
route_params = route_config.get("query_params", [])
|
|
156
|
+
|
|
157
|
+
# Get names already defined at route level
|
|
158
|
+
route_param_names = set()
|
|
159
|
+
for item in route_params:
|
|
160
|
+
if isinstance(item, dict) and len(item) == 1:
|
|
161
|
+
route_param_names.add(next(iter(item)))
|
|
162
|
+
|
|
163
|
+
# Append non-overridden top-level params after route params
|
|
164
|
+
merged = list(route_params)
|
|
165
|
+
for item in top_level:
|
|
166
|
+
if isinstance(item, dict) and len(item) == 1:
|
|
167
|
+
name = next(iter(item))
|
|
168
|
+
if name not in route_param_names:
|
|
169
|
+
merged.append(item)
|
|
170
|
+
|
|
171
|
+
return {**route_config, "query_params": merged}
|
|
172
|
+
|
|
173
|
+
|
|
135
174
|
def get_named_query(query_name: str, database_config: Dict[str, Any]) -> Optional[str]:
|
|
136
175
|
"""Get a named query from database configuration.
|
|
137
176
|
|
|
@@ -239,6 +278,77 @@ def find_database_route(path: str, database_config: Dict[str, Any]) -> Optional[
|
|
|
239
278
|
return None
|
|
240
279
|
|
|
241
280
|
|
|
281
|
+
def validate_route_config(route_config: Dict[str, Any]) -> bool:
|
|
282
|
+
"""Validate route configuration with new declarative parameter format.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
route_config: Route configuration dictionary.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if configuration is valid, False otherwise.
|
|
289
|
+
"""
|
|
290
|
+
# Validate basic route structure
|
|
291
|
+
if not isinstance(route_config, dict):
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
# Check for required route field
|
|
295
|
+
if 'route' not in route_config:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# Validate query_params structure if present
|
|
299
|
+
if 'query_params' in route_config:
|
|
300
|
+
query_params = route_config['query_params']
|
|
301
|
+
if not isinstance(query_params, list):
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
for param_item in query_params:
|
|
305
|
+
if not isinstance(param_item, dict):
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
# Each parameter should have exactly one key (the parameter name)
|
|
309
|
+
if len(param_item) != 1:
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
param_name, param_config = next(iter(param_item.items()))
|
|
313
|
+
|
|
314
|
+
# Validate parameter configuration structure
|
|
315
|
+
if not isinstance(param_config, dict):
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
# Check for valid configuration keys
|
|
319
|
+
valid_keys = {'sql', 'sql_append', 'response', 'conditional', 'action', 'required', 'default', 'missing_response'}
|
|
320
|
+
if not any(key in param_config for key in valid_keys):
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
# Validate conditional structure if present
|
|
324
|
+
if 'conditional' in param_config:
|
|
325
|
+
conditional = param_config['conditional']
|
|
326
|
+
if not isinstance(conditional, dict):
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
# Each conditional value should have sql, response, or action
|
|
330
|
+
for condition_key, condition_config in conditional.items():
|
|
331
|
+
if not isinstance(condition_config, dict):
|
|
332
|
+
return False
|
|
333
|
+
valid_condition_keys = {'sql', 'response', 'action'}
|
|
334
|
+
if not any(key in condition_config for key in valid_condition_keys):
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
# Validate action structure if present
|
|
338
|
+
if 'action' in param_config:
|
|
339
|
+
action = param_config['action']
|
|
340
|
+
if not isinstance(action, (str, dict)):
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
# Validate missing_response structure if present
|
|
344
|
+
if 'missing_response' in param_config:
|
|
345
|
+
missing_response = param_config['missing_response']
|
|
346
|
+
if not isinstance(missing_response, dict):
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
|
|
242
352
|
#
|
|
243
353
|
# INTERNAL
|
|
244
354
|
#
|
|
@@ -106,7 +106,8 @@ def _add_remote_routes(app: FastAPI, route_mapper: RouteMapper) -> None:
|
|
|
106
106
|
# Handle as database route
|
|
107
107
|
success, response_data, status_code, error_message = await route_mapper.map_database_route(
|
|
108
108
|
database_name=remote_name,
|
|
109
|
-
path=path
|
|
109
|
+
path=path,
|
|
110
|
+
query_params=dict(request.query_params)
|
|
110
111
|
)
|
|
111
112
|
else:
|
|
112
113
|
# Handle as remote API route
|
|
@@ -101,7 +101,8 @@ def _add_remote_routes(app: Flask, route_mapper: RouteMapper) -> None:
|
|
|
101
101
|
success, response_data, status_code, error_message = asyncio.run(
|
|
102
102
|
route_mapper.map_database_route(
|
|
103
103
|
database_name=remote_name,
|
|
104
|
-
path=""
|
|
104
|
+
path="",
|
|
105
|
+
query_params=dict(request.args)
|
|
105
106
|
)
|
|
106
107
|
)
|
|
107
108
|
else:
|
|
@@ -145,7 +146,8 @@ def _add_remote_routes(app: Flask, route_mapper: RouteMapper) -> None:
|
|
|
145
146
|
success, response_data, status_code, error_message = asyncio.run(
|
|
146
147
|
route_mapper.map_database_route(
|
|
147
148
|
database_name=remote_name,
|
|
148
|
-
path=path
|
|
149
|
+
path=path,
|
|
150
|
+
query_params=dict(request.args)
|
|
149
151
|
)
|
|
150
152
|
)
|
|
151
153
|
else:
|