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.
Files changed (34) hide show
  1. {api_dock-0.2.2 → api_dock-0.3.0}/PKG-INFO +213 -1
  2. {api_dock-0.2.2 → api_dock-0.3.0}/README.md +212 -0
  3. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/config.py +61 -0
  4. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/database_config.py +110 -0
  5. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/fast_api.py +2 -1
  6. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/flask_api.py +4 -2
  7. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/route_mapper.py +31 -7
  8. api_dock-0.3.0/api_dock/sql_builder.py +628 -0
  9. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/PKG-INFO +213 -1
  10. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/SOURCES.txt +3 -1
  11. api_dock-0.3.0/config/databases/test_users.yaml +127 -0
  12. {api_dock-0.2.2 → api_dock-0.3.0}/pyproject.toml +2 -1
  13. api_dock-0.3.0/tests/test_sql_append.py +451 -0
  14. api_dock-0.2.2/api_dock/sql_builder.py +0 -163
  15. {api_dock-0.2.2 → api_dock-0.3.0}/LICENSE.md +0 -0
  16. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/__init__.py +0 -0
  17. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/cli.py +0 -0
  18. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/config_discovery.py +0 -0
  19. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock/storage_auth.py +0 -0
  20. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/dependency_links.txt +0 -0
  21. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/entry_points.txt +0 -0
  22. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/requires.txt +0 -0
  23. {api_dock-0.2.2 → api_dock-0.3.0}/api_dock.egg-info/top_level.txt +0 -0
  24. {api_dock-0.2.2 → api_dock-0.3.0}/config/config.yaml +0 -0
  25. {api_dock-0.2.2 → api_dock-0.3.0}/config/databases/db_example.yaml +0 -0
  26. {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_allowed_routes.yaml +0 -0
  27. {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_custom_mapping.yaml +0 -0
  28. {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_restrictions.yaml +0 -0
  29. {api_dock-0.2.2 → api_dock-0.3.0}/config/remotes/remote_with_wildcards.yaml +0 -0
  30. {api_dock-0.2.2 → api_dock-0.3.0}/setup.cfg +0 -0
  31. {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_config_syntax.py +0 -0
  32. {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_curl_fixes.py +0 -0
  33. {api_dock-0.2.2 → api_dock-0.3.0}/tests/test_restrictions.py +0 -0
  34. {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.2.2
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: