oplnk-python-utils 1.1.0__tar.gz → 1.1.2__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 (21) hide show
  1. {oplnk_python_utils-1.1.0/oplnk_python_utils.egg-info → oplnk_python_utils-1.1.2}/PKG-INFO +121 -6
  2. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/README.md +120 -5
  3. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/__init__.py +1 -1
  4. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/parsing.py +90 -11
  5. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/query/query_builder.py +267 -31
  6. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2/oplnk_python_utils.egg-info}/PKG-INFO +121 -6
  7. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/setup.py +2 -2
  8. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/LICENSE +0 -0
  9. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/MANIFEST.in +0 -0
  10. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/cache/__init__.py +0 -0
  11. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/cache/cache_utils.py +0 -0
  12. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/__init__.py +0 -0
  13. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/data.py +0 -0
  14. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/core/serialization.py +0 -0
  15. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils/query/__init__.py +0 -0
  16. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/SOURCES.txt +0 -0
  17. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/dependency_links.txt +0 -0
  18. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/not-zip-safe +0 -0
  19. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/requires.txt +0 -0
  20. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/oplnk_python_utils.egg-info/top_level.txt +0 -0
  21. {oplnk_python_utils-1.1.0 → oplnk_python_utils-1.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oplnk-python-utils
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
5
5
  Home-page: https://github.com/openlinkspa/oplnk-python-utils
6
6
  Author: OpenLink SpA
@@ -149,15 +149,50 @@ keys = parse_key_string_to_list("filters[name][$eq]")
149
149
 
150
150
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
151
151
  # Result: {"filters": {"name": {"$eq": "john"}}}
152
+
153
+ # Enhanced field path handling for MongoDB aggregation (NEW)
154
+ from oplnk_python_utils.core.parsing import (
155
+ normalize_field_path,
156
+ validate_field_path,
157
+ build_aggregation_field_expression
158
+ )
159
+
160
+ # Normalize field paths for MongoDB aggregation
161
+ normalized = normalize_field_path("establishment_type[0].name")
162
+ # Result: "establishment_type.0.name" - MongoDB compatible path
163
+
164
+ normalized = normalize_field_path("user.addresses[1].street")
165
+ # Result: "user.addresses.1.street" - Array notation converted to dot notation
166
+
167
+ # Validate field paths for security and correctness
168
+ valid = validate_field_path("user.profile.name") # Returns: True
169
+ valid = validate_field_path("user$invalid") # Returns: False
170
+ valid = validate_field_path("user.settings.theme") # Returns: True
171
+ valid = validate_field_path("") # Returns: False
172
+
173
+ # Build proper aggregation field expressions
174
+ field_expr = build_aggregation_field_expression("user.name")
175
+ # Result: "$user.name" - Ready for MongoDB aggregation pipeline
176
+
177
+ field_expr = build_aggregation_field_expression("establishment_type.0.name")
178
+ # Result: "$establishment_type.0.name" - Handles normalized array paths
152
179
  ```
153
180
 
154
181
  #### Parsing Functions
155
182
 
183
+ **Main Functions:**
184
+
156
185
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
157
186
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
158
187
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
159
188
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
160
189
 
190
+ **Field Path Utilities (NEW):**
191
+
192
+ - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
193
+ - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
194
+ - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
195
+
161
196
  ### Serialization Utilities (`core.serialization`)
162
197
 
163
198
  Functions for converting data types to serializable formats.
@@ -351,7 +386,7 @@ GET /users?clear_cache=true
351
386
 
352
387
  ## Query Module
353
388
 
354
- The query module provides a MongoDB aggregation pipeline builder with advanced features.
389
+ The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
355
390
 
356
391
  ### Usage
357
392
 
@@ -431,16 +466,42 @@ print(results)
431
466
  # "data": [...],
432
467
  # "meta": {"totalCount": 150}
433
468
  # }
469
+
470
+ # Enhanced support for nested fields and array notation (NEW)
471
+ nested_query = {
472
+ "filters": {
473
+ "user.profile.name": {"$containsi": "john"}, # Dot notation
474
+ "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
475
+ "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
476
+ "tags": {"$listContains": "featured"}, # Array contains
477
+ "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
478
+ },
479
+ "sort": [
480
+ "user.profile.created_at:desc", # Sort by nested field
481
+ "establishment_type.0.priority:asc" # Sort by array element
482
+ ],
483
+ "fields": [
484
+ "user.profile.name", # Project nested fields
485
+ "addresses.0.street", # Project specific array element
486
+ "meta.settings" # Project nested objects
487
+ ]
488
+ }
489
+
490
+ # All field paths are automatically validated and normalized for MongoDB compatibility
491
+ results = query_builder.get_query_response(
492
+ db_collection=collection,
493
+ query=nested_query
494
+ )
434
495
  ```
435
496
 
436
497
  ### QueryBuilder Methods
437
498
 
438
499
  #### Pipeline Builders
439
500
 
440
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
441
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
442
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
443
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
501
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
502
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
503
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
504
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
444
505
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
445
506
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
446
507
 
@@ -459,6 +520,7 @@ print(results)
459
520
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
460
521
 
461
522
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
523
+ - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
462
524
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
463
525
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
464
526
 
@@ -597,6 +659,59 @@ async def create_user(request: Request, user_data: dict):
597
659
 
598
660
  ## Advanced Features
599
661
 
662
+ ### Enhanced Nested Field Support (NEW)
663
+
664
+ The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
665
+
666
+ #### Supported Field Notations
667
+
668
+ ```python
669
+ # Dot notation for nested objects
670
+ "user.profile.name" # Access nested object properties
671
+ "settings.theme.colors.primary" # Deep nesting support
672
+
673
+ # Array notation (both styles supported)
674
+ "addresses[0].street" # Bracket notation - automatically normalized
675
+ "addresses.0.street" # Dot notation - MongoDB native format
676
+ "items[2].details.price" # Mixed notation support
677
+
678
+ # Complex nested structures
679
+ "metadata.tags[1].category.name" # Arrays within nested objects
680
+ "stores.0.products[5].variants.color" # Multiple levels of nesting
681
+ ```
682
+
683
+ #### Automatic Field Validation
684
+
685
+ All field paths are automatically validated for:
686
+
687
+ - **Security**: Prevents injection of invalid MongoDB operators
688
+ - **Syntax**: Ensures MongoDB-compatible field syntax
689
+ - **Normalization**: Converts bracket notation to dot notation
690
+ - **Error Handling**: Graceful handling of invalid paths with logging
691
+
692
+ ```python
693
+ # Examples of validation in action
694
+ query = {
695
+ "filters": {
696
+ "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
697
+ "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
698
+ "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
699
+ "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
700
+ "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
701
+ }
702
+ }
703
+
704
+ # Invalid fields are automatically filtered out with helpful logging
705
+ # Valid fields continue to work normally
706
+ ```
707
+
708
+ #### Performance Optimizations
709
+
710
+ - **Smart Field Processing**: Only processes fields that need normalization
711
+ - **Validation Caching**: Avoids repeated validation of the same field paths
712
+ - **Error Recovery**: Continues processing even when individual fields are invalid
713
+ - **Minimal Overhead**: Validation adds negligible performance impact
714
+
600
715
  ### Custom Relation Definitions
601
716
 
602
717
  ```python
@@ -107,15 +107,50 @@ keys = parse_key_string_to_list("filters[name][$eq]")
107
107
 
108
108
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
109
109
  # Result: {"filters": {"name": {"$eq": "john"}}}
110
+
111
+ # Enhanced field path handling for MongoDB aggregation (NEW)
112
+ from oplnk_python_utils.core.parsing import (
113
+ normalize_field_path,
114
+ validate_field_path,
115
+ build_aggregation_field_expression
116
+ )
117
+
118
+ # Normalize field paths for MongoDB aggregation
119
+ normalized = normalize_field_path("establishment_type[0].name")
120
+ # Result: "establishment_type.0.name" - MongoDB compatible path
121
+
122
+ normalized = normalize_field_path("user.addresses[1].street")
123
+ # Result: "user.addresses.1.street" - Array notation converted to dot notation
124
+
125
+ # Validate field paths for security and correctness
126
+ valid = validate_field_path("user.profile.name") # Returns: True
127
+ valid = validate_field_path("user$invalid") # Returns: False
128
+ valid = validate_field_path("user.settings.theme") # Returns: True
129
+ valid = validate_field_path("") # Returns: False
130
+
131
+ # Build proper aggregation field expressions
132
+ field_expr = build_aggregation_field_expression("user.name")
133
+ # Result: "$user.name" - Ready for MongoDB aggregation pipeline
134
+
135
+ field_expr = build_aggregation_field_expression("establishment_type.0.name")
136
+ # Result: "$establishment_type.0.name" - Handles normalized array paths
110
137
  ```
111
138
 
112
139
  #### Parsing Functions
113
140
 
141
+ **Main Functions:**
142
+
114
143
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
115
144
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
116
145
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
117
146
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
118
147
 
148
+ **Field Path Utilities (NEW):**
149
+
150
+ - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
151
+ - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
152
+ - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
153
+
119
154
  ### Serialization Utilities (`core.serialization`)
120
155
 
121
156
  Functions for converting data types to serializable formats.
@@ -309,7 +344,7 @@ GET /users?clear_cache=true
309
344
 
310
345
  ## Query Module
311
346
 
312
- The query module provides a MongoDB aggregation pipeline builder with advanced features.
347
+ The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
313
348
 
314
349
  ### Usage
315
350
 
@@ -389,16 +424,42 @@ print(results)
389
424
  # "data": [...],
390
425
  # "meta": {"totalCount": 150}
391
426
  # }
427
+
428
+ # Enhanced support for nested fields and array notation (NEW)
429
+ nested_query = {
430
+ "filters": {
431
+ "user.profile.name": {"$containsi": "john"}, # Dot notation
432
+ "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
433
+ "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
434
+ "tags": {"$listContains": "featured"}, # Array contains
435
+ "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
436
+ },
437
+ "sort": [
438
+ "user.profile.created_at:desc", # Sort by nested field
439
+ "establishment_type.0.priority:asc" # Sort by array element
440
+ ],
441
+ "fields": [
442
+ "user.profile.name", # Project nested fields
443
+ "addresses.0.street", # Project specific array element
444
+ "meta.settings" # Project nested objects
445
+ ]
446
+ }
447
+
448
+ # All field paths are automatically validated and normalized for MongoDB compatibility
449
+ results = query_builder.get_query_response(
450
+ db_collection=collection,
451
+ query=nested_query
452
+ )
392
453
  ```
393
454
 
394
455
  ### QueryBuilder Methods
395
456
 
396
457
  #### Pipeline Builders
397
458
 
398
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
399
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
400
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
401
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
459
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
460
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
461
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
462
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
402
463
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
403
464
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
404
465
 
@@ -417,6 +478,7 @@ print(results)
417
478
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
418
479
 
419
480
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
481
+ - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
420
482
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
421
483
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
422
484
 
@@ -555,6 +617,59 @@ async def create_user(request: Request, user_data: dict):
555
617
 
556
618
  ## Advanced Features
557
619
 
620
+ ### Enhanced Nested Field Support (NEW)
621
+
622
+ The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
623
+
624
+ #### Supported Field Notations
625
+
626
+ ```python
627
+ # Dot notation for nested objects
628
+ "user.profile.name" # Access nested object properties
629
+ "settings.theme.colors.primary" # Deep nesting support
630
+
631
+ # Array notation (both styles supported)
632
+ "addresses[0].street" # Bracket notation - automatically normalized
633
+ "addresses.0.street" # Dot notation - MongoDB native format
634
+ "items[2].details.price" # Mixed notation support
635
+
636
+ # Complex nested structures
637
+ "metadata.tags[1].category.name" # Arrays within nested objects
638
+ "stores.0.products[5].variants.color" # Multiple levels of nesting
639
+ ```
640
+
641
+ #### Automatic Field Validation
642
+
643
+ All field paths are automatically validated for:
644
+
645
+ - **Security**: Prevents injection of invalid MongoDB operators
646
+ - **Syntax**: Ensures MongoDB-compatible field syntax
647
+ - **Normalization**: Converts bracket notation to dot notation
648
+ - **Error Handling**: Graceful handling of invalid paths with logging
649
+
650
+ ```python
651
+ # Examples of validation in action
652
+ query = {
653
+ "filters": {
654
+ "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
655
+ "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
656
+ "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
657
+ "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
658
+ "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
659
+ }
660
+ }
661
+
662
+ # Invalid fields are automatically filtered out with helpful logging
663
+ # Valid fields continue to work normally
664
+ ```
665
+
666
+ #### Performance Optimizations
667
+
668
+ - **Smart Field Processing**: Only processes fields that need normalization
669
+ - **Validation Caching**: Avoids repeated validation of the same field paths
670
+ - **Error Recovery**: Continues processing even when individual fields are invalid
671
+ - **Minimal Overhead**: Validation adds negligible performance impact
672
+
558
673
  ### Custom Relation Definitions
559
674
 
560
675
  ```python
@@ -7,7 +7,7 @@ from . import core
7
7
  from . import cache
8
8
  from . import query
9
9
 
10
- __version__ = "1.1.0"
10
+ __version__ = "1.1.2"
11
11
  __author__ = "OpenLink SpA"
12
12
 
13
13
  __all__ = ["core", "cache", "query", "__version__"]
@@ -11,10 +11,10 @@ from .data import merge_dicts
11
11
  def infer_type(value: Union[str, List, Any]) -> Any:
12
12
  """
13
13
  Infer the correct Python type from a string value.
14
-
14
+
15
15
  Args:
16
16
  value: The value to convert (string, list, or other)
17
-
17
+
18
18
  Returns:
19
19
  Value converted to appropriate Python type
20
20
  """
@@ -54,10 +54,10 @@ def infer_type(value: Union[str, List, Any]) -> Any:
54
54
  def parse_key_string_to_list(s: str) -> List[str]:
55
55
  """
56
56
  Parse a string like 'filters[name][$containsi]' into ['filters', 'name', '$containsi'].
57
-
57
+
58
58
  Args:
59
59
  s: String with bracketed notation
60
-
60
+
61
61
  Returns:
62
62
  List of parsed key components
63
63
  """
@@ -69,11 +69,11 @@ def parse_key_string_to_list(s: str) -> List[str]:
69
69
  def parse_list_to_dict(lst: List[str], value: str) -> Dict[str, Any]:
70
70
  """
71
71
  Convert a list of keys and a value into a nested dictionary.
72
-
72
+
73
73
  Args:
74
74
  lst: List of keys for nested structure
75
75
  value: Value to place at the deepest level
76
-
76
+
77
77
  Returns:
78
78
  Nested dictionary structure
79
79
  """
@@ -93,16 +93,16 @@ def parse_params_to_dict(params: Union[Dict[str, str], Any]) -> Dict[str, Any]:
93
93
  """
94
94
  Parse query parameters into a structured dictionary.
95
95
  Handles nested parameters like filters[name][$containsi]=john
96
-
96
+
97
97
  Args:
98
98
  params: Query parameters (FastAPI QueryParams or dict-like object)
99
-
99
+
100
100
  Returns:
101
101
  Structured dictionary with nested parameters
102
102
  """
103
103
  # Handle FastAPI QueryParams or similar objects
104
- if hasattr(params, 'items'):
105
- param_dict = dict(params.items()) if hasattr(params, 'items') else dict(params)
104
+ if hasattr(params, "items"):
105
+ param_dict = dict(params.items()) if hasattr(params, "items") else dict(params)
106
106
  else:
107
107
  param_dict = params
108
108
 
@@ -111,4 +111,83 @@ def parse_params_to_dict(params: Union[Dict[str, str], Any]) -> Dict[str, Any]:
111
111
  args = parse_key_string_to_list(key)
112
112
  dictionary = parse_list_to_dict(args, value)
113
113
  result = merge_dicts(result, dictionary)
114
- return result
114
+ return result
115
+
116
+
117
+ # Field handling utilities for MongoDB aggregation pipelines
118
+
119
+
120
+ def normalize_field_path(field: str) -> str:
121
+ """
122
+ Normalize field path for MongoDB aggregation pipeline.
123
+ Handles dot notation and prepares for array notation.
124
+
125
+ Examples:
126
+ "establishment_type.name" -> "establishment_type.name"
127
+ "establishment_type[0].name" -> "establishment_type.0.name"
128
+ "user.addresses[1].street" -> "user.addresses.1.street"
129
+
130
+ Args:
131
+ field: Field path to normalize
132
+
133
+ Returns:
134
+ Normalized field path compatible with MongoDB aggregation
135
+ """
136
+ # Convert array notation to dot notation for MongoDB
137
+ # establishment_type[0].name -> establishment_type.0.name
138
+
139
+ # Pattern to match array notation like [0], [1], etc.
140
+ array_pattern = r"\[(\d+)\]"
141
+ normalized = re.sub(array_pattern, r".\1", field)
142
+
143
+ return normalized
144
+
145
+
146
+ def validate_field_path(field: str) -> bool:
147
+ """
148
+ Validate if a field path is valid for MongoDB aggregation.
149
+
150
+ Args:
151
+ field: Field path to validate
152
+
153
+ Returns:
154
+ bool: True if valid, False otherwise
155
+ """
156
+ if not field or not isinstance(field, str):
157
+ return False
158
+
159
+ # Check for invalid characters
160
+ invalid_chars = ["$", " ", "\t", "\n", "\r"]
161
+ if any(
162
+ char in field
163
+ for char in invalid_chars
164
+ if char != "$" or not field.startswith("$")
165
+ ):
166
+ return False
167
+
168
+ return True
169
+
170
+
171
+ def build_aggregation_field_expression(field: str) -> str:
172
+ """
173
+ Build proper MongoDB aggregation field expression.
174
+
175
+ Args:
176
+ field: Field name or path
177
+
178
+ Returns:
179
+ str: Properly formatted field expression for aggregation
180
+
181
+ Raises:
182
+ ValueError: If field path is invalid
183
+ """
184
+ if not validate_field_path(field):
185
+ raise ValueError(f"Invalid field path: {field}")
186
+
187
+ normalized_field = normalize_field_path(field)
188
+
189
+ # If field doesn't start with $, add it for aggregation
190
+ if not normalized_field.startswith("$"):
191
+ return f"${normalized_field}"
192
+
193
+ return normalized_field
@@ -3,18 +3,18 @@ Query Builder - Generic query building functionality for MongoDB aggregation pip
3
3
  """
4
4
 
5
5
  import re
6
+ import time
6
7
  from copy import deepcopy
7
- from typing import List, Dict, Any, Optional, Union, Callable
8
- from datetime import datetime
9
- from bson import ObjectId
8
+ from typing import List, Dict, Any, Optional, Union
10
9
  from fastapi import HTTPException, status
10
+ from pprint import pprint
11
11
 
12
12
  # Import core utilities
13
13
  from ..core.parsing import (
14
- infer_type,
15
- parse_key_string_to_list,
16
- parse_list_to_dict,
17
14
  parse_params_to_dict,
15
+ normalize_field_path,
16
+ validate_field_path,
17
+ build_aggregation_field_expression,
18
18
  )
19
19
 
20
20
 
@@ -46,6 +46,7 @@ class QueryBuilder:
46
46
  cache_client=None,
47
47
  cache_key_prefix: str = "",
48
48
  clear_cache: bool = False,
49
+ debug: bool = False,
49
50
  ) -> Dict[str, Any]:
50
51
  """
51
52
  Complete query method that handles parameter parsing and query execution.
@@ -60,8 +61,25 @@ class QueryBuilder:
60
61
  cache_client: Cache client (Redis)
61
62
  cache_key_prefix: Prefix for cache keys
62
63
  clear_cache: Whether to clear cache before query
64
+ debug: Whether to enable debug output
63
65
  """
66
+ # Start timing
67
+ start_time = time.time()
68
+
69
+ if debug:
70
+ print("🔵 [DEBUG] Parámetros de entrada:")
71
+ pprint(
72
+ {
73
+ "params": params if params is not None else "None",
74
+ "query": query if query is not None else "None",
75
+ "use_cache": use_cache,
76
+ "cache_key_prefix": cache_key_prefix,
77
+ "clear_cache": clear_cache,
78
+ }
79
+ )
80
+
64
81
  # Parse params if provided, otherwise use query directly
82
+ parsing_start_time = time.time()
65
83
  if params is not None:
66
84
  parsed_query = parse_params_to_dict(params)
67
85
  elif query is not None:
@@ -73,7 +91,14 @@ class QueryBuilder:
73
91
  if "clear_cache" in parsed_query:
74
92
  clear_cache = parsed_query.pop("clear_cache", False)
75
93
 
76
- self.get_query_response(
94
+ parsing_time = time.time() - parsing_start_time
95
+
96
+ if debug:
97
+ print("🔵 [DEBUG] Query parseada:")
98
+ pprint(parsed_query)
99
+ print(f"🔵 [DEBUG] Tiempo de parsing: {parsing_time:.3f}s")
100
+
101
+ return self.get_query_response(
77
102
  db_collection,
78
103
  parsed_query,
79
104
  pipeline,
@@ -82,6 +107,9 @@ class QueryBuilder:
82
107
  cache_key_prefix=cache_key_prefix,
83
108
  use_cache=use_cache,
84
109
  clear_cache=clear_cache,
110
+ debug=debug,
111
+ start_time=start_time,
112
+ parsing_time=parsing_time,
85
113
  )
86
114
 
87
115
  def build_filter_pipeline(self, filters: Dict[str, Any]) -> Dict[str, Any]:
@@ -421,6 +449,9 @@ class QueryBuilder:
421
449
  field_name = list(condition.keys())[0]
422
450
  field_conditions = condition[field_name]
423
451
 
452
+ # Normalize field name for dot notation support
453
+ normalized_field_name = normalize_field_path(field_name)
454
+
424
455
  if isinstance(field_conditions, dict):
425
456
  operator = list(field_conditions.keys())[0]
426
457
  value = field_conditions[operator]
@@ -443,38 +474,68 @@ class QueryBuilder:
443
474
  # Expand this condition into multiple alternatives
444
475
  value_type, str_value = get_value_type_and_string(value)
445
476
  expanded_conditions = expand_text_search_condition(
446
- field_name, operator, str_value
477
+ normalized_field_name, operator, str_value
447
478
  )
448
479
 
449
480
  # Add all expanded conditions to the $or
450
481
  or_conditions.extend(expanded_conditions)
451
482
  else:
452
- # Regular condition, process normally
453
- sub_pipeline = self.build_filter_pipeline(condition)
483
+ # Regular condition, process normally with normalized field
484
+ normalized_condition = {
485
+ normalized_field_name: field_conditions
486
+ }
487
+ sub_pipeline = self.build_filter_pipeline(
488
+ normalized_condition
489
+ )
454
490
  or_conditions.append(sub_pipeline)
455
491
  else:
456
- # Regular condition, process normally
457
- sub_pipeline = self.build_filter_pipeline(condition)
492
+ # Regular condition, process normally with normalized field
493
+ normalized_condition = {
494
+ normalized_field_name: field_conditions
495
+ }
496
+ sub_pipeline = self.build_filter_pipeline(
497
+ normalized_condition
498
+ )
458
499
  or_conditions.append(sub_pipeline)
459
500
 
460
501
  pipeline["$or"] = or_conditions
461
502
  case "$and":
462
503
  and_conditions = []
463
504
  for condition in conditions:
464
- sub_pipeline = self.build_filter_pipeline(condition)
505
+ # Normalize field names in condition for dot notation support
506
+ normalized_condition = {}
507
+ for field_name, field_conditions in condition.items():
508
+ normalized_field_name = normalize_field_path(field_name)
509
+ normalized_condition[normalized_field_name] = (
510
+ field_conditions
511
+ )
512
+
513
+ sub_pipeline = self.build_filter_pipeline(normalized_condition)
465
514
  and_conditions.append(sub_pipeline)
466
515
  pipeline["$and"] = and_conditions
467
516
  case _:
468
517
  try:
469
- sub_pipeline = build_sub_pipeline(conditions, field)
518
+ # Normalize field path for dot notation and array notation support
519
+ normalized_field = normalize_field_path(field)
520
+
521
+ # Validate the field path
522
+ if not validate_field_path(normalized_field):
523
+ print(
524
+ f"Warning: Invalid field path '{field}', skipping filter condition"
525
+ )
526
+ continue
527
+
528
+ sub_pipeline = build_sub_pipeline(conditions, normalized_field)
470
529
 
471
530
  if isinstance(sub_pipeline, dict):
472
531
  if "_field" in sub_pipeline:
473
- new_field = f"{field}.{sub_pipeline['_field']}"
532
+ new_field = (
533
+ f"{normalized_field}.{sub_pipeline['_field']}"
534
+ )
474
535
  pipeline[new_field] = sub_pipeline["_conditions"]
475
536
  continue
476
537
 
477
- pipeline[field] = sub_pipeline
538
+ pipeline[normalized_field] = sub_pipeline
478
539
  except (ValueError, TypeError) as e:
479
540
  # Log the error and skip this filter condition
480
541
  print(f"Error processing filter for field '{field}': {e}")
@@ -483,22 +544,64 @@ class QueryBuilder:
483
544
  return pipeline
484
545
 
485
546
  def build_sort_pipeline(self, sort_params: List[str]) -> Dict[str, Any]:
486
- """Build sort pipeline from sort parameters"""
547
+ """
548
+ Build sort pipeline from sort parameters with support for dot notation and array notation.
549
+ """
487
550
  sort_pipeline = {}
488
551
  for item in sort_params:
489
- field, order = item.split(":")
490
- sort_pipeline[field] = 1 if order == "asc" else -1
552
+ try:
553
+ field, order = item.split(":")
554
+ normalized_field = normalize_field_path(field)
555
+
556
+ if validate_field_path(normalized_field):
557
+ sort_pipeline[normalized_field] = 1 if order == "asc" else -1
558
+ else:
559
+ print(f"Warning: Invalid field path '{field}', skipping in sort")
560
+ except ValueError as e:
561
+ print(
562
+ f"Warning: Invalid sort parameter '{item}', expected format 'field:order': {e}"
563
+ )
564
+ except Exception as e:
565
+ print(f"Warning: Error processing sort parameter '{item}': {e}")
566
+
491
567
  return {"$sort": sort_pipeline}
492
568
 
493
569
  def build_fields_pipeline(
494
570
  self, fields_params: List[str], populate_params: Optional[List] = None
495
571
  ) -> Dict[str, Any]:
496
- """Build fields projection pipeline"""
497
- fields_pipeline = {field: 1 for field in fields_params}
572
+ """
573
+ Build fields projection pipeline with support for dot notation and array notation.
574
+ """
575
+ fields_pipeline = {}
576
+
577
+ # Process fields_params with normalization
578
+ for field in fields_params:
579
+ try:
580
+ normalized_field = normalize_field_path(field)
581
+ if validate_field_path(normalized_field):
582
+ fields_pipeline[normalized_field] = 1
583
+ else:
584
+ print(
585
+ f"Warning: Invalid field path '{field}', skipping in projection"
586
+ )
587
+ except Exception as e:
588
+ print(f"Warning: Error processing field '{field}' in projection: {e}")
498
589
 
590
+ # Process populate_params if provided
499
591
  if populate_params:
500
592
  for field in populate_params:
501
- fields_pipeline[field] = 1
593
+ try:
594
+ normalized_field = normalize_field_path(field)
595
+ if validate_field_path(normalized_field):
596
+ fields_pipeline[normalized_field] = 1
597
+ else:
598
+ print(
599
+ f"Warning: Invalid populate field path '{field}', skipping in projection"
600
+ )
601
+ except Exception as e:
602
+ print(
603
+ f"Warning: Error processing populate field '{field}' in projection: {e}"
604
+ )
502
605
 
503
606
  return {"$project": fields_pipeline}
504
607
 
@@ -508,14 +611,46 @@ class QueryBuilder:
508
611
  include_project: bool = True,
509
612
  pipeline: Optional[List] = None,
510
613
  ) -> List[Dict[str, Any]]:
511
- """Build distinct pipeline"""
512
- group_id = {field: f"${field}" for field in distinct_params}
513
- group_stage = {"$group": {"_id": group_id}}
514
- if include_project:
515
- replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
516
- return [group_stage, replace_root_stage]
517
- else:
518
- return [group_stage]
614
+ """
615
+ Build distinct pipeline with support for dot notation and array notation.
616
+ """
617
+ try:
618
+ # Build group_id with proper field expressions
619
+ group_id = {}
620
+ for field in distinct_params:
621
+ try:
622
+ # Use utility function to build proper aggregation expression
623
+ field_expression = build_aggregation_field_expression(field)
624
+ # Use original field name as key for the result
625
+ group_id[field] = field_expression
626
+ except ValueError as e:
627
+ # Log error and skip invalid field
628
+ print(f"Warning: Skipping invalid field '{field}' in distinct: {e}")
629
+ continue
630
+
631
+ if not group_id:
632
+ raise ValueError("No valid fields provided for distinct operation")
633
+
634
+ group_stage = {"$group": {"_id": group_id}}
635
+
636
+ if include_project:
637
+ replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
638
+ return [group_stage, replace_root_stage]
639
+ else:
640
+ return [group_stage]
641
+
642
+ except Exception as e:
643
+ # Fallback to original behavior for backwards compatibility
644
+ print(
645
+ f"Warning: Error in enhanced distinct pipeline, falling back to original: {e}"
646
+ )
647
+ group_id = {field: f"${field}" for field in distinct_params}
648
+ group_stage = {"$group": {"_id": group_id}}
649
+ if include_project:
650
+ replace_root_stage = {"$replaceRoot": {"newRoot": "$_id"}}
651
+ return [group_stage, replace_root_stage]
652
+ else:
653
+ return [group_stage]
519
654
 
520
655
  def build_pagination_pipeline(
521
656
  self, pagination_params: Dict[str, Any], pipeline: Optional[List] = None
@@ -883,6 +1018,9 @@ class QueryBuilder:
883
1018
  cache_key_prefix: str = "",
884
1019
  use_cache: bool = False,
885
1020
  clear_cache: bool = False,
1021
+ debug: bool = False,
1022
+ start_time: float = 0,
1023
+ parsing_time: float = 0,
886
1024
  ) -> Dict[str, Any]:
887
1025
  """
888
1026
  Get query response with optional caching support
@@ -896,16 +1034,64 @@ class QueryBuilder:
896
1034
  cache_key_prefix: Prefix for cache keys
897
1035
  use_cache: Whether to use caching
898
1036
  clear_cache: Whether to clear cache before query
1037
+ debug: Whether to enable debug output
1038
+ start_time: Start time of the query
1039
+ parsing_time: Time spent parsing parameters
899
1040
  """
900
1041
  try:
1042
+ # Build pipelines
1043
+ pipeline_build_start = time.time()
901
1044
  base_pipeline = self.make_aggregation_query(query, pipeline)
902
1045
  count_pipeline = self.make_count_aggregation_query(query, pipeline_count)
1046
+ pipeline_build_time = time.time() - pipeline_build_start
1047
+
1048
+ if debug:
1049
+ print("🔵 [DEBUG] Pipeline de agregación construido:")
1050
+ pprint(base_pipeline)
1051
+ print("🔵 [DEBUG] Pipeline de count construido:")
1052
+ pprint(count_pipeline)
1053
+
1054
+ # Debug filters
1055
+ if "filters" in query:
1056
+ filter_pipeline = self.build_filter_pipeline(query["filters"])
1057
+ print("🔵 [DEBUG] Filtros aplicados:")
1058
+ pprint(filter_pipeline)
1059
+
1060
+ # Debug special operations
1061
+ if "populate" in query:
1062
+ print("🔵 [DEBUG] Relaciones a popular:")
1063
+ pprint(query["populate"])
1064
+
1065
+ if "distinct" in query:
1066
+ print("🔵 [DEBUG] Campos distinct:")
1067
+ pprint(query["distinct"])
1068
+
1069
+ if "sort" in query:
1070
+ print("🔵 [DEBUG] Ordenamiento aplicado:")
1071
+ pprint(query["sort"])
1072
+
1073
+ if "pagination" in query:
1074
+ print("🔵 [DEBUG] Paginación aplicada:")
1075
+ pprint(query["pagination"])
1076
+
1077
+ # Start MongoDB query timing
1078
+ mongodb_start_time = time.time()
903
1079
 
904
1080
  # Get data
905
1081
  data_result = list(db_collection.aggregate(base_pipeline))
906
1082
 
907
1083
  # Get count
908
1084
  if use_cache and cache_client:
1085
+ if debug:
1086
+ print("🔵 [DEBUG] Configuración de cache:")
1087
+ pprint(
1088
+ {
1089
+ "cache_key_prefix": cache_key_prefix,
1090
+ "clear_cache": clear_cache,
1091
+ "use_cache": use_cache,
1092
+ }
1093
+ )
1094
+
909
1095
  # Import cache utilities
910
1096
  from ..cache import (
911
1097
  generate_cache_id,
@@ -919,6 +1105,10 @@ class QueryBuilder:
919
1105
  query_hash = generate_cache_id(query_for_hash)
920
1106
  count_key = f"{cache_key_prefix}:query:count:{query_hash}"
921
1107
 
1108
+ if debug:
1109
+ print("🔵 [DEBUG] Cache key generada:")
1110
+ print(f"count_key: {count_key}")
1111
+
922
1112
  # Get or set meta from cache
923
1113
  meta_result = get_or_set_cache(
924
1114
  cache_client,
@@ -935,8 +1125,54 @@ class QueryBuilder:
935
1125
  else:
936
1126
  meta_result = list(db_collection.aggregate(count_pipeline))
937
1127
 
938
- return self.sanitize_result(query, data_result, meta_result)
1128
+ # End MongoDB query timing
1129
+ mongodb_time = time.time() - mongodb_start_time
1130
+
1131
+ if debug:
1132
+ print("🔵 [DEBUG] Resultado de query de datos:")
1133
+ print(f"Registros encontrados: {len(data_result)}")
1134
+ if data_result:
1135
+ print("Muestra del primer registro:")
1136
+ pprint(data_result[0])
1137
+ if len(data_result) > 1:
1138
+ print(f"... y {len(data_result) - 1} registros más")
1139
+ else:
1140
+ print("Sin datos encontrados")
1141
+
1142
+ print("🔵 [DEBUG] Resultado de count:")
1143
+ pprint(meta_result)
1144
+
1145
+ result = self.sanitize_result(query, data_result, meta_result)
1146
+
1147
+ if debug:
1148
+ # Calculate total time
1149
+ total_time = time.time() - start_time
1150
+
1151
+ print("🔵 [DEBUG] Respuesta final sanitizada:")
1152
+ preview_data = result.get("data", [])
1153
+ data_preview = {
1154
+ "data_count": len(preview_data),
1155
+ "first_record": preview_data[0] if preview_data else None,
1156
+ "remaining_records": (
1157
+ f"... y {len(preview_data) - 1} más"
1158
+ if len(preview_data) > 1
1159
+ else "ninguno más"
1160
+ ),
1161
+ "meta": result.get("meta", {}),
1162
+ "has_pagination": "pagination" in query,
1163
+ }
1164
+ pprint(data_preview)
1165
+
1166
+ print("🔵 [DEBUG] ⏱️ MÉTRICAS DE PERFORMANCE:")
1167
+ print(f" 📊 Tiempo de parsing/pre-consulta: {parsing_time:.3f}s")
1168
+ print(f" 🗄️ Tiempo real de consulta MongoDB: {mongodb_time:.3f}s")
1169
+ print(f" 🚀 Tiempo total de procesamiento: {total_time:.3f}s")
1170
+ print("=" * 60)
1171
+
1172
+ return result
939
1173
  except Exception as e:
1174
+ if debug:
1175
+ print(f"🔴 [DEBUG] Error en get_query_response: {str(e)}")
940
1176
  raise HTTPException(
941
1177
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
942
1178
  detail=str(e),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oplnk-python-utils
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Reusable utilities for caching, data manipulation, parsing, and MongoDB query building
5
5
  Home-page: https://github.com/openlinkspa/oplnk-python-utils
6
6
  Author: OpenLink SpA
@@ -149,15 +149,50 @@ keys = parse_key_string_to_list("filters[name][$eq]")
149
149
 
150
150
  nested_dict = parse_list_to_dict(["filters", "name", "$eq"], "john")
151
151
  # Result: {"filters": {"name": {"$eq": "john"}}}
152
+
153
+ # Enhanced field path handling for MongoDB aggregation (NEW)
154
+ from oplnk_python_utils.core.parsing import (
155
+ normalize_field_path,
156
+ validate_field_path,
157
+ build_aggregation_field_expression
158
+ )
159
+
160
+ # Normalize field paths for MongoDB aggregation
161
+ normalized = normalize_field_path("establishment_type[0].name")
162
+ # Result: "establishment_type.0.name" - MongoDB compatible path
163
+
164
+ normalized = normalize_field_path("user.addresses[1].street")
165
+ # Result: "user.addresses.1.street" - Array notation converted to dot notation
166
+
167
+ # Validate field paths for security and correctness
168
+ valid = validate_field_path("user.profile.name") # Returns: True
169
+ valid = validate_field_path("user$invalid") # Returns: False
170
+ valid = validate_field_path("user.settings.theme") # Returns: True
171
+ valid = validate_field_path("") # Returns: False
172
+
173
+ # Build proper aggregation field expressions
174
+ field_expr = build_aggregation_field_expression("user.name")
175
+ # Result: "$user.name" - Ready for MongoDB aggregation pipeline
176
+
177
+ field_expr = build_aggregation_field_expression("establishment_type.0.name")
178
+ # Result: "$establishment_type.0.name" - Handles normalized array paths
152
179
  ```
153
180
 
154
181
  #### Parsing Functions
155
182
 
183
+ **Main Functions:**
184
+
156
185
  - **`parse_params_to_dict(params)`**: Parse FastAPI QueryParams or dict into structured nested dictionary
157
186
  - **`infer_type(value)`**: Smart type inference from string values (supports int, float, bool, datetime, lists)
158
187
  - **`parse_key_string_to_list(s)`**: Parse bracketed notation strings like `"filters[name][$eq]"`
159
188
  - **`parse_list_to_dict(lst, value)`**: Convert key list and value into nested dictionary structure
160
189
 
190
+ **Field Path Utilities (NEW):**
191
+
192
+ - **`normalize_field_path(field)`**: Normalize field paths for MongoDB aggregation with dot notation and array support
193
+ - **`validate_field_path(field)`**: Validate if a field path is valid for MongoDB aggregation pipelines
194
+ - **`build_aggregation_field_expression(field)`**: Build proper MongoDB aggregation field expressions with $ prefix
195
+
161
196
  ### Serialization Utilities (`core.serialization`)
162
197
 
163
198
  Functions for converting data types to serializable formats.
@@ -351,7 +386,7 @@ GET /users?clear_cache=true
351
386
 
352
387
  ## Query Module
353
388
 
354
- The query module provides a MongoDB aggregation pipeline builder with advanced features.
389
+ The query module provides a MongoDB aggregation pipeline builder with advanced features including enhanced support for nested fields, dot notation, and array notation.
355
390
 
356
391
  ### Usage
357
392
 
@@ -431,16 +466,42 @@ print(results)
431
466
  # "data": [...],
432
467
  # "meta": {"totalCount": 150}
433
468
  # }
469
+
470
+ # Enhanced support for nested fields and array notation (NEW)
471
+ nested_query = {
472
+ "filters": {
473
+ "user.profile.name": {"$containsi": "john"}, # Dot notation
474
+ "establishment_type.0.name": {"$eq": "restaurant"}, # Array index notation
475
+ "addresses[1].street": {"$startsWith": "Main"}, # Bracket notation (auto-normalized)
476
+ "tags": {"$listContains": "featured"}, # Array contains
477
+ "meta.settings.theme": {"$eq": "dark"} # Deep nested fields
478
+ },
479
+ "sort": [
480
+ "user.profile.created_at:desc", # Sort by nested field
481
+ "establishment_type.0.priority:asc" # Sort by array element
482
+ ],
483
+ "fields": [
484
+ "user.profile.name", # Project nested fields
485
+ "addresses.0.street", # Project specific array element
486
+ "meta.settings" # Project nested objects
487
+ ]
488
+ }
489
+
490
+ # All field paths are automatically validated and normalized for MongoDB compatibility
491
+ results = query_builder.get_query_response(
492
+ db_collection=collection,
493
+ query=nested_query
494
+ )
434
495
  ```
435
496
 
436
497
  ### QueryBuilder Methods
437
498
 
438
499
  #### Pipeline Builders
439
500
 
440
- - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline
441
- - **`build_sort_pipeline(sort_params)`**: Build sort pipeline
442
- - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline
443
- - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline
501
+ - **`build_filter_pipeline(filters)`**: Build MongoDB filter pipeline with enhanced support for nested fields and array notation
502
+ - **`build_sort_pipeline(sort_params)`**: Build sort pipeline with dot notation and array index support
503
+ - **`build_fields_pipeline(fields_params)`**: Build field projection pipeline with nested field validation
504
+ - **`build_distinct_pipeline(distinct_params)`**: Build distinct pipeline with proper field expression handling
444
505
  - **`build_pagination_pipeline(pagination_params)`**: Build pagination pipeline
445
506
  - **`build_populate_pipeline(populate_params)`**: Build population pipeline
446
507
 
@@ -459,6 +520,7 @@ print(results)
459
520
  The QueryBuilder class leverages utilities from the core module for enhanced functionality:
460
521
 
461
522
  - **Parameter parsing**: Uses `core.parsing.parse_params_to_dict()` for automatic query parameter parsing
523
+ - **Field path processing**: Uses `core.parsing.normalize_field_path()`, `validate_field_path()`, and `build_aggregation_field_expression()` for safe nested field handling
462
524
  - **Data normalization**: Uses `core.data.normalize()` for consistent cache key generation
463
525
  - **Datetime serialization**: Uses `core.serialization.datetime_serializer()` for cache-friendly data
464
526
 
@@ -597,6 +659,59 @@ async def create_user(request: Request, user_data: dict):
597
659
 
598
660
  ## Advanced Features
599
661
 
662
+ ### Enhanced Nested Field Support (NEW)
663
+
664
+ The query builder now provides comprehensive support for nested fields, dot notation, and array notation with automatic validation and normalization.
665
+
666
+ #### Supported Field Notations
667
+
668
+ ```python
669
+ # Dot notation for nested objects
670
+ "user.profile.name" # Access nested object properties
671
+ "settings.theme.colors.primary" # Deep nesting support
672
+
673
+ # Array notation (both styles supported)
674
+ "addresses[0].street" # Bracket notation - automatically normalized
675
+ "addresses.0.street" # Dot notation - MongoDB native format
676
+ "items[2].details.price" # Mixed notation support
677
+
678
+ # Complex nested structures
679
+ "metadata.tags[1].category.name" # Arrays within nested objects
680
+ "stores.0.products[5].variants.color" # Multiple levels of nesting
681
+ ```
682
+
683
+ #### Automatic Field Validation
684
+
685
+ All field paths are automatically validated for:
686
+
687
+ - **Security**: Prevents injection of invalid MongoDB operators
688
+ - **Syntax**: Ensures MongoDB-compatible field syntax
689
+ - **Normalization**: Converts bracket notation to dot notation
690
+ - **Error Handling**: Graceful handling of invalid paths with logging
691
+
692
+ ```python
693
+ # Examples of validation in action
694
+ query = {
695
+ "filters": {
696
+ "user.profile.name": {"$eq": "John"}, # ✅ Valid - passes through
697
+ "user$invalid": {"$eq": "test"}, # ❌ Invalid - skipped with warning
698
+ "settings[0].theme": {"$eq": "dark"}, # ✅ Valid - normalized to "settings.0.theme"
699
+ "": {"$eq": "empty"}, # ❌ Invalid - skipped with warning
700
+ "normal_field": {"$eq": "value"} # ✅ Valid - standard field access
701
+ }
702
+ }
703
+
704
+ # Invalid fields are automatically filtered out with helpful logging
705
+ # Valid fields continue to work normally
706
+ ```
707
+
708
+ #### Performance Optimizations
709
+
710
+ - **Smart Field Processing**: Only processes fields that need normalization
711
+ - **Validation Caching**: Avoids repeated validation of the same field paths
712
+ - **Error Recovery**: Continues processing even when individual fields are invalid
713
+ - **Minimal Overhead**: Validation adds negligible performance impact
714
+
600
715
  ### Custom Relation Definitions
601
716
 
602
717
  ```python
@@ -2,11 +2,11 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="oplnk-python-utils",
5
- version="1.1.0",
5
+ version="1.1.2",
6
6
  author="OpenLink SpA",
7
7
  author_email="contacto@openlink.cl",
8
8
  description="Reusable utilities for caching, data manipulation, parsing, and MongoDB query building",
9
- long_description=open("README.md").read(),
9
+ long_description=open("README.md", encoding="utf-8").read(),
10
10
  long_description_content_type="text/markdown",
11
11
  url="https://github.com/openlinkspa/oplnk-python-utils",
12
12
  packages=find_packages(),