mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__py3-none-any.whl
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.
- mdb_engine/__init__.py +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/core/types.py
CHANGED
|
@@ -8,8 +8,7 @@ throughout the codebase.
|
|
|
8
8
|
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import
|
|
12
|
-
TypedDict, Union)
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
|
13
12
|
|
|
14
13
|
if TYPE_CHECKING:
|
|
15
14
|
from ..memory import Mem0MemoryService
|
|
@@ -42,20 +41,20 @@ class IndexDefinitionDict(TypedDict, total=False):
|
|
|
42
41
|
"partial",
|
|
43
42
|
"hybrid",
|
|
44
43
|
]
|
|
45
|
-
keys:
|
|
44
|
+
keys: dict[str, int | str] | list[list[str | int]]
|
|
46
45
|
unique: bool
|
|
47
46
|
sparse: bool
|
|
48
47
|
background: bool
|
|
49
48
|
expireAfterSeconds: int
|
|
50
|
-
partialFilterExpression:
|
|
51
|
-
weights:
|
|
49
|
+
partialFilterExpression: dict[str, Any]
|
|
50
|
+
weights: dict[str, int] # For text indexes
|
|
52
51
|
default_language: str # For text indexes
|
|
53
52
|
language_override: str # For text indexes
|
|
54
53
|
textIndexVersion: int # For text indexes
|
|
55
54
|
# Vector search specific
|
|
56
|
-
fields:
|
|
55
|
+
fields: list[dict[str, Any]] # For vectorSearch indexes
|
|
57
56
|
# Search index specific
|
|
58
|
-
mappings:
|
|
57
|
+
mappings: dict[str, Any] # For search indexes
|
|
59
58
|
# Geospatial specific
|
|
60
59
|
bucketSize: float # For geoHaystack
|
|
61
60
|
# TTL specific
|
|
@@ -80,12 +79,12 @@ class AuthAuthorizationDict(TypedDict, total=False):
|
|
|
80
79
|
model: str
|
|
81
80
|
policies_collection: str
|
|
82
81
|
link_users_roles: bool
|
|
83
|
-
default_roles:
|
|
82
|
+
default_roles: list[str]
|
|
84
83
|
# OSO-specific
|
|
85
|
-
api_key:
|
|
86
|
-
url:
|
|
87
|
-
initial_roles:
|
|
88
|
-
initial_policies:
|
|
84
|
+
api_key: str | None
|
|
85
|
+
url: str | None
|
|
86
|
+
initial_roles: list[dict[str, str]]
|
|
87
|
+
initial_policies: list[dict[str, str]]
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
class AuthPolicyDict(TypedDict, total=False):
|
|
@@ -94,12 +93,12 @@ class AuthPolicyDict(TypedDict, total=False):
|
|
|
94
93
|
required: bool
|
|
95
94
|
provider: Literal["casbin", "oso", "custom"]
|
|
96
95
|
authorization: AuthAuthorizationDict
|
|
97
|
-
allowed_roles:
|
|
98
|
-
allowed_users:
|
|
99
|
-
denied_users:
|
|
100
|
-
required_permissions:
|
|
96
|
+
allowed_roles: list[str]
|
|
97
|
+
allowed_users: list[str]
|
|
98
|
+
denied_users: list[str]
|
|
99
|
+
required_permissions: list[str]
|
|
101
100
|
custom_resource: str
|
|
102
|
-
custom_actions:
|
|
101
|
+
custom_actions: list[Literal["access", "read", "write", "admin"]]
|
|
103
102
|
allow_anonymous: bool
|
|
104
103
|
owner_can_access: bool
|
|
105
104
|
|
|
@@ -112,7 +111,7 @@ class DemoUserDict(TypedDict, total=False):
|
|
|
112
111
|
role: str
|
|
113
112
|
auto_create: bool
|
|
114
113
|
link_to_platform: bool
|
|
115
|
-
extra_data:
|
|
114
|
+
extra_data: dict[str, Any]
|
|
116
115
|
|
|
117
116
|
|
|
118
117
|
class UsersConfigDict(TypedDict, total=False):
|
|
@@ -127,7 +126,7 @@ class UsersConfigDict(TypedDict, total=False):
|
|
|
127
126
|
link_platform_users: bool
|
|
128
127
|
anonymous_user_prefix: str
|
|
129
128
|
user_id_field: str
|
|
130
|
-
demo_users:
|
|
129
|
+
demo_users: list[DemoUserDict]
|
|
131
130
|
auto_link_platform_demo: bool
|
|
132
131
|
demo_user_seed_strategy: Literal["auto", "manual", "disabled"]
|
|
133
132
|
enable_demo_user_access: bool
|
|
@@ -313,7 +312,7 @@ class MetricsConfigDict(TypedDict, total=False):
|
|
|
313
312
|
enabled: bool
|
|
314
313
|
collect_operation_metrics: bool
|
|
315
314
|
collect_performance_metrics: bool
|
|
316
|
-
custom_metrics:
|
|
315
|
+
custom_metrics: list[str]
|
|
317
316
|
|
|
318
317
|
|
|
319
318
|
class LoggingConfigDict(TypedDict, total=False):
|
|
@@ -342,13 +341,11 @@ class CORSConfigDict(TypedDict, total=False):
|
|
|
342
341
|
"""CORS configuration."""
|
|
343
342
|
|
|
344
343
|
enabled: bool
|
|
345
|
-
allow_origins:
|
|
344
|
+
allow_origins: list[str]
|
|
346
345
|
allow_credentials: bool
|
|
347
|
-
allow_methods:
|
|
348
|
-
|
|
349
|
-
]
|
|
350
|
-
allow_headers: List[str]
|
|
351
|
-
expose_headers: List[str]
|
|
346
|
+
allow_methods: list[Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "*"]]
|
|
347
|
+
allow_headers: list[str]
|
|
348
|
+
expose_headers: list[str]
|
|
352
349
|
max_age: int
|
|
353
350
|
|
|
354
351
|
|
|
@@ -374,22 +371,22 @@ class ManifestDict(TypedDict, total=False):
|
|
|
374
371
|
schema_version: str
|
|
375
372
|
slug: str
|
|
376
373
|
name: str
|
|
377
|
-
description:
|
|
374
|
+
description: str | None
|
|
378
375
|
status: Literal["active", "draft", "archived", "inactive"]
|
|
379
376
|
auth_required: bool # Backward compatibility
|
|
380
|
-
auth:
|
|
381
|
-
token_management:
|
|
382
|
-
data_scope:
|
|
383
|
-
pip_deps:
|
|
384
|
-
managed_indexes:
|
|
385
|
-
collection_settings:
|
|
386
|
-
websockets:
|
|
387
|
-
embedding_config:
|
|
388
|
-
memory_config:
|
|
389
|
-
cors:
|
|
390
|
-
observability:
|
|
391
|
-
initial_data:
|
|
392
|
-
developer_id:
|
|
377
|
+
auth: AuthConfigDict | None
|
|
378
|
+
token_management: TokenManagementDict | None
|
|
379
|
+
data_scope: list[str]
|
|
380
|
+
pip_deps: list[str]
|
|
381
|
+
managed_indexes: ManagedIndexesDict | None
|
|
382
|
+
collection_settings: dict[str, dict[str, Any]] | None
|
|
383
|
+
websockets: WebSocketsDict | None
|
|
384
|
+
embedding_config: EmbeddingConfigDict | None
|
|
385
|
+
memory_config: MemoryConfigDict | None
|
|
386
|
+
cors: CORSConfigDict | None
|
|
387
|
+
observability: ObservabilityConfigDict | None
|
|
388
|
+
initial_data: InitialDataDict | None
|
|
389
|
+
developer_id: str | None
|
|
393
390
|
|
|
394
391
|
|
|
395
392
|
# ============================================================================
|
|
@@ -401,13 +398,13 @@ class HealthStatusDict(TypedDict, total=False):
|
|
|
401
398
|
"""Health status response."""
|
|
402
399
|
|
|
403
400
|
status: Literal["healthy", "degraded", "unhealthy"]
|
|
404
|
-
checks:
|
|
401
|
+
checks: dict[str, dict[str, Any]]
|
|
405
402
|
timestamp: str
|
|
406
403
|
|
|
407
404
|
|
|
408
405
|
class MetricsDict(TypedDict, total=False):
|
|
409
406
|
"""Metrics response."""
|
|
410
407
|
|
|
411
|
-
operations:
|
|
412
|
-
summary:
|
|
408
|
+
operations: dict[str, dict[str, Any]]
|
|
409
|
+
summary: dict[str, Any]
|
|
413
410
|
timestamp: str
|
mdb_engine/database/README.md
CHANGED
|
@@ -180,9 +180,9 @@ Async-native interface for managing Atlas Search and Vector indexes.
|
|
|
180
180
|
```python
|
|
181
181
|
from mdb_engine.database import AsyncAtlasIndexManager
|
|
182
182
|
|
|
183
|
-
# Get index manager from collection
|
|
183
|
+
# Get index manager from collection (automatically uses unscoped collection)
|
|
184
184
|
collection = db.my_collection
|
|
185
|
-
index_manager =
|
|
185
|
+
index_manager = collection.index_manager # Secure way to access index manager
|
|
186
186
|
|
|
187
187
|
# Create vector search index
|
|
188
188
|
await index_manager.create_vector_search_index(
|
|
@@ -277,25 +277,121 @@ await index_manager.create_search_index(
|
|
|
277
277
|
)
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
##
|
|
280
|
+
## Security Features
|
|
281
|
+
|
|
282
|
+
The database module includes comprehensive security controls to prevent unauthorized access, NoSQL injection, resource exhaustion, and ensure data isolation:
|
|
283
|
+
|
|
284
|
+
### Query Security
|
|
285
|
+
|
|
286
|
+
All queries are automatically validated for security:
|
|
287
|
+
- **Dangerous operator blocking**: Blocks `$where`, `$eval`, `$function`, and `$accumulator` operators that allow JavaScript execution
|
|
288
|
+
- **Query depth limits**: Prevents deeply nested queries that could cause performance issues
|
|
289
|
+
- **Regex complexity limits**: Prevents ReDoS (Regular Expression Denial of Service) attacks
|
|
290
|
+
- **Pipeline validation**: Validates aggregation pipelines for safety and complexity
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
# These queries will be blocked:
|
|
294
|
+
db.collection.find({"$where": "this.status === 'active'"}) # ❌ Dangerous operator
|
|
295
|
+
db.collection.aggregate([{"$match": {}}] * 100) # ❌ Too many pipeline stages
|
|
281
296
|
|
|
282
|
-
|
|
297
|
+
# These queries are safe:
|
|
298
|
+
db.collection.find({"status": "active"}) # ✅ Safe
|
|
299
|
+
db.collection.find({"age": {"$gt": 18}}) # ✅ Safe
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Resource Limits
|
|
283
303
|
|
|
284
|
-
|
|
304
|
+
All operations have automatic resource limits to prevent resource exhaustion:
|
|
305
|
+
- **Query timeouts**: All queries automatically have `maxTimeMS` set (default: 30 seconds, max: 5 minutes)
|
|
306
|
+
- **Result size limits**: Maximum 10,000 documents per query (configurable)
|
|
307
|
+
- **Batch size limits**: Maximum 1,000 documents per cursor batch
|
|
308
|
+
- **Document size validation**: Documents are validated before insert (16MB MongoDB limit)
|
|
285
309
|
|
|
286
310
|
```python
|
|
287
|
-
|
|
311
|
+
# Timeouts are automatically enforced:
|
|
312
|
+
db.collection.find({"status": "active"}) # Automatically has maxTimeMS=30000
|
|
313
|
+
|
|
314
|
+
# Result limits are enforced:
|
|
315
|
+
db.collection.find({}, limit=20000) # Automatically capped to 10,000
|
|
316
|
+
|
|
317
|
+
# Document sizes are validated:
|
|
318
|
+
large_doc = {"data": "x" * (20 * 1024 * 1024)} # ❌ Exceeds 16MB limit
|
|
319
|
+
await db.collection.insert_one(large_doc) # Raises ResourceLimitExceeded
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Collection Name Validation
|
|
288
323
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
324
|
+
All collection names are validated for security:
|
|
325
|
+
|
|
326
|
+
### Collection Name Validation
|
|
327
|
+
|
|
328
|
+
All collection names are validated for security:
|
|
329
|
+
- **Format validation**: Must match MongoDB naming rules (alphanumeric, underscore, dot, hyphen)
|
|
330
|
+
- **Length limits**: 1-255 characters
|
|
331
|
+
- **Reserved names**: System collections (`apps_config`) are blocked
|
|
332
|
+
- **Reserved prefixes**: Collections starting with `system`, `admin`, `config`, or `local` are blocked
|
|
333
|
+
- **Path traversal protection**: Blocks attempts to use `..`, `/`, or `\` in collection names
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
# These will raise ValueError:
|
|
337
|
+
db.system_users # Reserved prefix
|
|
338
|
+
db.apps_config # Reserved name
|
|
339
|
+
db["../other"] # Path traversal attempt
|
|
340
|
+
db["123invalid"] # Invalid format (starts with number)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Cross-App Access Control
|
|
344
|
+
|
|
345
|
+
Cross-app collection access is strictly controlled:
|
|
346
|
+
- Apps can only read from collections of apps listed in their `read_scopes`
|
|
347
|
+
- Unauthorized cross-app access attempts are logged and blocked
|
|
348
|
+
- All cross-app access is logged for audit purposes
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
# App can only read from authorized apps
|
|
352
|
+
db = engine.get_scoped_db(
|
|
353
|
+
"my_app",
|
|
354
|
+
read_scopes=["my_app", "shared_app"] # Can read from these apps
|
|
294
355
|
)
|
|
295
356
|
|
|
296
|
-
|
|
357
|
+
# This works (authorized):
|
|
358
|
+
collection = db.get_collection("shared_app_data")
|
|
359
|
+
|
|
360
|
+
# This fails (unauthorized):
|
|
361
|
+
collection = db.get_collection("other_app_data") # Raises ValueError
|
|
297
362
|
```
|
|
298
363
|
|
|
364
|
+
### Scope Validation
|
|
365
|
+
|
|
366
|
+
The `get_scoped_db()` method validates all scopes:
|
|
367
|
+
- `read_scopes` must be a non-empty list of valid app slugs
|
|
368
|
+
- `write_scope` must be a non-empty string
|
|
369
|
+
- Invalid scopes raise `ValueError` with clear error messages
|
|
370
|
+
|
|
371
|
+
### Audit Logging
|
|
372
|
+
|
|
373
|
+
All security-relevant events are logged:
|
|
374
|
+
- Invalid collection name attempts
|
|
375
|
+
- Unauthorized cross-app access attempts
|
|
376
|
+
- Reserved name/prefix access attempts
|
|
377
|
+
- Collection name validation failures
|
|
378
|
+
|
|
379
|
+
Logs include app context (app_slug, collection_name, action) for security monitoring.
|
|
380
|
+
|
|
381
|
+
### Best Practices
|
|
382
|
+
|
|
383
|
+
1. **Always use scoped databases**: Never access raw MongoDB clients or databases
|
|
384
|
+
2. **Validate collection names**: Use descriptive, valid collection names
|
|
385
|
+
3. **Limit cross-app access**: Only grant `read_scopes` to apps that need cross-app data
|
|
386
|
+
4. **Monitor audit logs**: Review security logs regularly for suspicious patterns
|
|
387
|
+
5. **Follow naming conventions**: Use lowercase, underscore-separated names (e.g., `user_profiles`)
|
|
388
|
+
|
|
389
|
+
## Connection Pooling
|
|
390
|
+
|
|
391
|
+
The database module provides shared MongoDB connection pooling for efficient resource usage. Connection pooling is handled automatically by the engine - users should always use `engine.get_scoped_db()` for database access.
|
|
392
|
+
|
|
393
|
+
**Security Note:** Direct MongoDB client creation functions are internal and not part of the public API. Always use scoped databases to ensure proper app isolation.
|
|
394
|
+
|
|
299
395
|
### Pool Metrics
|
|
300
396
|
|
|
301
397
|
Monitor connection pool usage:
|
|
@@ -474,13 +570,40 @@ except OperationFailure as e:
|
|
|
474
570
|
print(f"MongoDB operation failed: {e.details}")
|
|
475
571
|
except AutoReconnect as e:
|
|
476
572
|
print(f"MongoDB reconnection: {e}")
|
|
477
|
-
except
|
|
478
|
-
print(f"
|
|
573
|
+
except (ConnectionFailure, ServerSelectionTimeoutError) as e:
|
|
574
|
+
print(f"Connection error: {e}")
|
|
479
575
|
```
|
|
480
576
|
|
|
481
577
|
## Integration Examples
|
|
482
578
|
|
|
483
|
-
### FastAPI Integration
|
|
579
|
+
### FastAPI Integration (Recommended)
|
|
580
|
+
|
|
581
|
+
Use the request-scoped `get_scoped_db` dependency from `mdb_engine.dependencies`:
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
from fastapi import Depends
|
|
585
|
+
from mdb_engine import MongoDBEngine
|
|
586
|
+
from mdb_engine.dependencies import get_scoped_db
|
|
587
|
+
|
|
588
|
+
engine = MongoDBEngine(mongo_uri="...", db_name="...")
|
|
589
|
+
app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
|
|
590
|
+
|
|
591
|
+
@app.get("/data")
|
|
592
|
+
async def get_data(db=Depends(get_scoped_db)):
|
|
593
|
+
# db is automatically scoped to "my_app"
|
|
594
|
+
docs = await db.my_collection.find({}).to_list(length=10)
|
|
595
|
+
return {"data": docs}
|
|
596
|
+
|
|
597
|
+
@app.post("/data")
|
|
598
|
+
async def create_data(db=Depends(get_scoped_db)):
|
|
599
|
+
# Writes are automatically scoped to "my_app"
|
|
600
|
+
result = await db.my_collection.insert_one({"name": "New Document"})
|
|
601
|
+
return {"inserted_id": str(result.inserted_id)}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Legacy FastAPI Integration
|
|
605
|
+
|
|
606
|
+
For apps not using `engine.create_app()`:
|
|
484
607
|
|
|
485
608
|
```python
|
|
486
609
|
from fastapi import FastAPI, Depends
|
|
@@ -508,8 +631,8 @@ db1 = engine.get_scoped_db("app1")
|
|
|
508
631
|
db2 = engine.get_scoped_db("app2")
|
|
509
632
|
|
|
510
633
|
# Cross-app read (read from app1, write to app2)
|
|
511
|
-
shared_db =
|
|
512
|
-
|
|
634
|
+
shared_db = engine.get_scoped_db(
|
|
635
|
+
app_slug="shared",
|
|
513
636
|
read_scopes=["app1", "app2"],
|
|
514
637
|
write_scope="shared"
|
|
515
638
|
)
|
mdb_engine/database/__init__.py
CHANGED
|
@@ -6,11 +6,20 @@ and MongoDB-style API for familiarity.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .abstraction import AppDB, Collection, get_app_db
|
|
9
|
-
from .connection import (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
from .connection import (
|
|
10
|
+
close_shared_client,
|
|
11
|
+
get_pool_metrics,
|
|
12
|
+
register_client_for_metrics,
|
|
13
|
+
verify_shared_client,
|
|
14
|
+
)
|
|
15
|
+
from .query_validator import QueryValidator
|
|
16
|
+
from .resource_limiter import ResourceLimiter
|
|
17
|
+
from .scoped_wrapper import (
|
|
18
|
+
AsyncAtlasIndexManager,
|
|
19
|
+
AutoIndexManager,
|
|
20
|
+
ScopedCollectionWrapper,
|
|
21
|
+
ScopedMongoWrapper,
|
|
22
|
+
)
|
|
14
23
|
|
|
15
24
|
__all__ = [
|
|
16
25
|
# Scoped wrappers
|
|
@@ -18,12 +27,14 @@ __all__ = [
|
|
|
18
27
|
"ScopedCollectionWrapper",
|
|
19
28
|
"AsyncAtlasIndexManager",
|
|
20
29
|
"AutoIndexManager",
|
|
30
|
+
# Query security
|
|
31
|
+
"QueryValidator",
|
|
32
|
+
"ResourceLimiter",
|
|
21
33
|
# Database abstraction
|
|
22
34
|
"AppDB",
|
|
23
35
|
"Collection",
|
|
24
36
|
"get_app_db",
|
|
25
37
|
# Connection pooling
|
|
26
|
-
"get_shared_mongo_client",
|
|
27
38
|
"verify_shared_client",
|
|
28
39
|
"get_pool_metrics",
|
|
29
40
|
"register_client_for_metrics",
|
|
@@ -24,15 +24,20 @@ Usage:
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
import logging
|
|
27
|
-
from
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from typing import Any
|
|
28
29
|
|
|
29
30
|
from ..exceptions import MongoDBEngineError
|
|
30
31
|
from .scoped_wrapper import ScopedMongoWrapper
|
|
31
32
|
|
|
32
33
|
try:
|
|
33
|
-
from pymongo.errors import (
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
from pymongo.errors import (
|
|
35
|
+
AutoReconnect,
|
|
36
|
+
ConnectionFailure,
|
|
37
|
+
InvalidOperation,
|
|
38
|
+
OperationFailure,
|
|
39
|
+
ServerSelectionTimeoutError,
|
|
40
|
+
)
|
|
36
41
|
except ImportError:
|
|
37
42
|
OperationFailure = Exception
|
|
38
43
|
AutoReconnect = Exception
|
|
@@ -41,8 +46,12 @@ except ImportError:
|
|
|
41
46
|
|
|
42
47
|
try:
|
|
43
48
|
from motor.motor_asyncio import AsyncIOMotorCursor
|
|
44
|
-
from pymongo.results import (
|
|
45
|
-
|
|
49
|
+
from pymongo.results import (
|
|
50
|
+
DeleteResult,
|
|
51
|
+
InsertManyResult,
|
|
52
|
+
InsertOneResult,
|
|
53
|
+
UpdateResult,
|
|
54
|
+
)
|
|
46
55
|
except ImportError:
|
|
47
56
|
AsyncIOMotorCursor = None
|
|
48
57
|
InsertOneResult = None
|
|
@@ -80,8 +89,8 @@ class Collection:
|
|
|
80
89
|
self._collection = scoped_collection
|
|
81
90
|
|
|
82
91
|
async def find_one(
|
|
83
|
-
self, filter:
|
|
84
|
-
) ->
|
|
92
|
+
self, filter: dict[str, Any] | None = None, *args, **kwargs
|
|
93
|
+
) -> dict[str, Any] | None:
|
|
85
94
|
"""
|
|
86
95
|
Find a single document matching the filter.
|
|
87
96
|
|
|
@@ -112,9 +121,7 @@ class Collection:
|
|
|
112
121
|
context={"operation": "find_one"},
|
|
113
122
|
) from e
|
|
114
123
|
|
|
115
|
-
def find(
|
|
116
|
-
self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
|
|
117
|
-
) -> AsyncIOMotorCursor:
|
|
124
|
+
def find(self, filter: dict[str, Any] | None = None, *args, **kwargs) -> AsyncIOMotorCursor:
|
|
118
125
|
"""
|
|
119
126
|
Find documents matching the filter.
|
|
120
127
|
|
|
@@ -140,9 +147,7 @@ class Collection:
|
|
|
140
147
|
"""
|
|
141
148
|
return self._collection.find(filter or {}, *args, **kwargs)
|
|
142
149
|
|
|
143
|
-
async def insert_one(
|
|
144
|
-
self, document: Dict[str, Any], *args, **kwargs
|
|
145
|
-
) -> InsertOneResult:
|
|
150
|
+
async def insert_one(self, document: dict[str, Any], *args, **kwargs) -> InsertOneResult:
|
|
146
151
|
"""
|
|
147
152
|
Insert a single document.
|
|
148
153
|
|
|
@@ -174,7 +179,7 @@ class Collection:
|
|
|
174
179
|
) from e
|
|
175
180
|
|
|
176
181
|
async def insert_many(
|
|
177
|
-
self, documents:
|
|
182
|
+
self, documents: list[dict[str, Any]], *args, **kwargs
|
|
178
183
|
) -> InsertManyResult:
|
|
179
184
|
"""
|
|
180
185
|
Insert multiple documents at once.
|
|
@@ -209,7 +214,7 @@ class Collection:
|
|
|
209
214
|
) from e
|
|
210
215
|
|
|
211
216
|
async def update_one(
|
|
212
|
-
self, filter:
|
|
217
|
+
self, filter: dict[str, Any], update: dict[str, Any], *args, **kwargs
|
|
213
218
|
) -> UpdateResult:
|
|
214
219
|
"""
|
|
215
220
|
Update a single document matching the filter.
|
|
@@ -247,7 +252,7 @@ class Collection:
|
|
|
247
252
|
) from e
|
|
248
253
|
|
|
249
254
|
async def update_many(
|
|
250
|
-
self, filter:
|
|
255
|
+
self, filter: dict[str, Any], update: dict[str, Any], *args, **kwargs
|
|
251
256
|
) -> UpdateResult:
|
|
252
257
|
"""
|
|
253
258
|
Update multiple documents matching the filter.
|
|
@@ -284,7 +289,7 @@ class Collection:
|
|
|
284
289
|
) from e
|
|
285
290
|
|
|
286
291
|
async def replace_one(
|
|
287
|
-
self, filter:
|
|
292
|
+
self, filter: dict[str, Any], replacement: dict[str, Any], *args, **kwargs
|
|
288
293
|
) -> UpdateResult:
|
|
289
294
|
"""
|
|
290
295
|
Replace a single document matching the filter.
|
|
@@ -363,7 +368,7 @@ class Collection:
|
|
|
363
368
|
context={"operation": "replace_one"},
|
|
364
369
|
) from e
|
|
365
370
|
|
|
366
|
-
async def delete_one(self, filter:
|
|
371
|
+
async def delete_one(self, filter: dict[str, Any], *args, **kwargs) -> DeleteResult:
|
|
367
372
|
"""
|
|
368
373
|
Delete a single document matching the filter.
|
|
369
374
|
|
|
@@ -396,7 +401,7 @@ class Collection:
|
|
|
396
401
|
) from e
|
|
397
402
|
|
|
398
403
|
async def delete_many(
|
|
399
|
-
self, filter:
|
|
404
|
+
self, filter: dict[str, Any] | None = None, *args, **kwargs
|
|
400
405
|
) -> DeleteResult:
|
|
401
406
|
"""
|
|
402
407
|
Delete multiple documents matching the filter.
|
|
@@ -428,9 +433,7 @@ class Collection:
|
|
|
428
433
|
context={"operation": "delete_many"},
|
|
429
434
|
) from e
|
|
430
435
|
|
|
431
|
-
async def count_documents(
|
|
432
|
-
self, filter: Optional[Dict[str, Any]] = None, *args, **kwargs
|
|
433
|
-
) -> int:
|
|
436
|
+
async def count_documents(self, filter: dict[str, Any] | None = None, *args, **kwargs) -> int:
|
|
434
437
|
"""
|
|
435
438
|
Count documents matching the filter.
|
|
436
439
|
|
|
@@ -460,9 +463,7 @@ class Collection:
|
|
|
460
463
|
context={"operation": "count_documents"},
|
|
461
464
|
) from e
|
|
462
465
|
|
|
463
|
-
def aggregate(
|
|
464
|
-
self, pipeline: List[Dict[str, Any]], *args, **kwargs
|
|
465
|
-
) -> AsyncIOMotorCursor:
|
|
466
|
+
def aggregate(self, pipeline: list[dict[str, Any]], *args, **kwargs) -> AsyncIOMotorCursor:
|
|
466
467
|
"""
|
|
467
468
|
Perform aggregation pipeline.
|
|
468
469
|
|
|
@@ -518,7 +519,7 @@ class AppDB:
|
|
|
518
519
|
raise RuntimeError("ScopedMongoWrapper is not available. Check imports.")
|
|
519
520
|
|
|
520
521
|
self._wrapper = scoped_wrapper
|
|
521
|
-
self._collection_cache:
|
|
522
|
+
self._collection_cache: dict[str, Collection] = {}
|
|
522
523
|
|
|
523
524
|
def collection(self, name: str) -> Collection:
|
|
524
525
|
"""
|
|
@@ -554,11 +555,17 @@ class AppDB:
|
|
|
554
555
|
Example:
|
|
555
556
|
db.users.get("user_123") # Instead of db.collection("users").get("user_123")
|
|
556
557
|
"""
|
|
557
|
-
#
|
|
558
|
-
if name
|
|
558
|
+
# Explicitly block access to 'database' property (removed for security)
|
|
559
|
+
if name == "database":
|
|
559
560
|
raise AttributeError(
|
|
560
|
-
|
|
561
|
+
"'database' property has been removed for security. "
|
|
562
|
+
"Use collection.index_manager for index operations. "
|
|
563
|
+
"All data access must go through scoped collections."
|
|
561
564
|
)
|
|
565
|
+
|
|
566
|
+
# Only proxy collection names, not internal attributes
|
|
567
|
+
if name.startswith("_"):
|
|
568
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
562
569
|
return self.collection(name)
|
|
563
570
|
|
|
564
571
|
@property
|
|
@@ -576,26 +583,6 @@ class AppDB:
|
|
|
576
583
|
"""
|
|
577
584
|
return self._wrapper
|
|
578
585
|
|
|
579
|
-
@property
|
|
580
|
-
def database(self):
|
|
581
|
-
"""
|
|
582
|
-
Access the underlying AsyncIOMotorDatabase (unscoped).
|
|
583
|
-
|
|
584
|
-
This is useful for advanced operations that need direct access to the
|
|
585
|
-
real database without scoping, such as index management or administrative
|
|
586
|
-
operations.
|
|
587
|
-
|
|
588
|
-
Returns:
|
|
589
|
-
The underlying AsyncIOMotorDatabase instance
|
|
590
|
-
|
|
591
|
-
Example:
|
|
592
|
-
# Access underlying database for index management
|
|
593
|
-
real_db = db.database
|
|
594
|
-
collection = real_db["my_collection"]
|
|
595
|
-
index_manager = AsyncAtlasIndexManager(collection)
|
|
596
|
-
"""
|
|
597
|
-
return self._wrapper.database
|
|
598
|
-
|
|
599
586
|
|
|
600
587
|
# FastAPI dependency helper
|
|
601
588
|
async def get_app_db(request, get_scoped_db_func: Callable) -> AppDB:
|