statezero 0.1.0b1__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.
Files changed (45) hide show
  1. statezero/__init__.py +0 -0
  2. statezero/adaptors/__init__.py +0 -0
  3. statezero/adaptors/django/__init__.py +0 -0
  4. statezero/adaptors/django/apps.py +97 -0
  5. statezero/adaptors/django/config.py +99 -0
  6. statezero/adaptors/django/context_manager.py +12 -0
  7. statezero/adaptors/django/event_emitters.py +78 -0
  8. statezero/adaptors/django/exception_handler.py +98 -0
  9. statezero/adaptors/django/extensions/__init__.py +0 -0
  10. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
  13. statezero/adaptors/django/f_handler.py +312 -0
  14. statezero/adaptors/django/helpers.py +153 -0
  15. statezero/adaptors/django/middleware.py +10 -0
  16. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  17. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  18. statezero/adaptors/django/migrations/__init__.py +0 -0
  19. statezero/adaptors/django/orm.py +915 -0
  20. statezero/adaptors/django/permissions.py +252 -0
  21. statezero/adaptors/django/query_optimizer.py +772 -0
  22. statezero/adaptors/django/schemas.py +324 -0
  23. statezero/adaptors/django/search_providers/__init__.py +0 -0
  24. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  25. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  26. statezero/adaptors/django/serializers.py +554 -0
  27. statezero/adaptors/django/urls.py +14 -0
  28. statezero/adaptors/django/views.py +336 -0
  29. statezero/core/__init__.py +34 -0
  30. statezero/core/ast_parser.py +821 -0
  31. statezero/core/ast_validator.py +266 -0
  32. statezero/core/classes.py +167 -0
  33. statezero/core/config.py +263 -0
  34. statezero/core/context_storage.py +4 -0
  35. statezero/core/event_bus.py +175 -0
  36. statezero/core/event_emitters.py +60 -0
  37. statezero/core/exceptions.py +106 -0
  38. statezero/core/interfaces.py +492 -0
  39. statezero/core/process_request.py +184 -0
  40. statezero/core/types.py +63 -0
  41. statezero-0.1.0b1.dist-info/METADATA +252 -0
  42. statezero-0.1.0b1.dist-info/RECORD +45 -0
  43. statezero-0.1.0b1.dist-info/WHEEL +5 -0
  44. statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
  45. statezero-0.1.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,492 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union, Literal, Protocol
5
+
6
+ from statezero.core.classes import ModelSchemaMetadata, SchemaFieldMetadata
7
+ from statezero.core.types import (ActionType, ORMField, ORMModel, ORMQuerySet, RequestType)
8
+
9
+ class AbstractORMProvider(ABC):
10
+ """
11
+ A merged ORM engine interface that combines both query building (filtering,
12
+ ordering, aggregation, etc.) and ORM provider responsibilities (queryset assembly,
13
+ event signal registration, model graph construction, etc.).
14
+ """
15
+
16
+ # === Query Engine Methods ===
17
+
18
+ @abstractmethod
19
+ def get_fields(self) -> Set[str]:
20
+ """
21
+ Get all of the model fields - doesn't apply permissions check.
22
+ """
23
+ pass
24
+
25
+ @abstractmethod
26
+ def filter_node(self, node: Dict[str, Any]) -> None:
27
+ """
28
+ Apply filter/and/or/not logic to the current query.
29
+ """
30
+ pass
31
+
32
+ @abstractmethod
33
+ def search_node(self, search_query: str, search_fields: Set[str]) -> None:
34
+ """
35
+ Apply search to the current query.
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def create(self, data: Dict[str, Any]) -> Any:
41
+ """Create a new record."""
42
+ pass
43
+
44
+ @abstractmethod
45
+ def update(self, node: Dict[str, Any]) -> int:
46
+ """
47
+ Update records (by filter or primary key).
48
+ Returns the number of rows updated.
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ def delete(self, node: Dict[str, Any]) -> int:
54
+ """
55
+ Delete records (by filter or primary key).
56
+ Returns the number of rows deleted.
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def get(self, node: Dict[str, Any]) -> Any:
62
+ """
63
+ Retrieve a single record. Raises an error if multiple or none are found.
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ def get_or_create(self, node: Dict[str, Any]) -> Tuple[Any, bool]:
69
+ """
70
+ Retrieve a record if it exists, otherwise create it.
71
+ Returns a tuple of (instance, created_flag).
72
+ """
73
+ pass
74
+
75
+ @abstractmethod
76
+ def update_or_create(self, node: Dict[str, Any]) -> Tuple[Any, bool]:
77
+ """
78
+ Update a record if it exists or create it if it doesn't.
79
+ Returns a tuple of (instance, created_flag).
80
+ """
81
+ pass
82
+
83
+ @abstractmethod
84
+ def first(self) -> Any:
85
+ """Return the first record from the current query."""
86
+ pass
87
+
88
+ @abstractmethod
89
+ def last(self) -> Any:
90
+ """Return the last record from the current query."""
91
+ pass
92
+
93
+ @abstractmethod
94
+ def exists(self) -> bool:
95
+ """Return True if the current query has any results; otherwise False."""
96
+ pass
97
+
98
+ @abstractmethod
99
+ def aggregate(self, agg_list: List[Dict[str, Any]]) -> Dict[str, Any]:
100
+ """
101
+ Aggregate the current query based on the provided functions.
102
+ Example:
103
+ [
104
+ {'function': 'count', 'field': 'id', 'alias': 'id_count'},
105
+ {'function': 'sum', 'field': 'price', 'alias': 'price_sum'}
106
+ ]
107
+ """
108
+ pass
109
+
110
+ @abstractmethod
111
+ def count(self, field: str) -> int:
112
+ """Count the number of records for the given field."""
113
+ pass
114
+
115
+ @abstractmethod
116
+ def sum(self, field: str) -> Any:
117
+ """Sum the values of the given field."""
118
+ pass
119
+
120
+ @abstractmethod
121
+ def avg(self, field: str) -> Any:
122
+ """Calculate the average of the given field."""
123
+ pass
124
+
125
+ @abstractmethod
126
+ def min(self, field: str) -> Any:
127
+ """Find the minimum value for the given field."""
128
+ pass
129
+
130
+ @abstractmethod
131
+ def max(self, field: str) -> Any:
132
+ """Find the maximum value for the given field."""
133
+ pass
134
+
135
+ @abstractmethod
136
+ def order_by(self, order_list: List[Dict[str, str]]) -> None:
137
+ """
138
+ Order the query based on a list of fields.
139
+ Each dict should contain 'field' and optionally 'direction' ('asc' or 'desc').
140
+ """
141
+ pass
142
+
143
+ @abstractmethod
144
+ def select_related(self, related_fields: List[str]) -> None:
145
+ """
146
+ Optimize the query by eager loading the given related fields.
147
+ """
148
+ pass
149
+
150
+ @abstractmethod
151
+ def prefetch_related(self, related_fields: List[str]) -> None:
152
+ """
153
+ Optimize the query by prefetching the given related fields.
154
+ """
155
+ pass
156
+
157
+ @abstractmethod
158
+ def fetch_list(self, offset: int, limit: int) -> List[Any]:
159
+ """
160
+ Return a list of records (as dicts or objects) based on pagination.
161
+ """
162
+ pass
163
+
164
+ # === ORM Provider Methods ===
165
+
166
+ @abstractmethod
167
+ def get_queryset(
168
+ self,
169
+ request: RequestType,
170
+ model: ORMModel, # type:ignore
171
+ initial_ast: Dict[str, Any],
172
+ custom_querysets: Dict[str, Type],
173
+ registered_permissions: List[Type],
174
+ ) -> Any:
175
+ """
176
+ Assemble and return the base QuerySet (or equivalent) for the given model.
177
+ This method considers the request context, initial AST (filters, sorting, etc.),
178
+ custom query sets, and any model-specific permission restrictions.
179
+ """
180
+ pass
181
+
182
+ @abstractmethod
183
+ def register_event_signals(self, event_emitter: Any) -> None:
184
+ """
185
+ Wire the ORM provider's signals so that on create, update, or delete events,
186
+ the global event emitter is invoked with the proper event type, instance,
187
+ and global event configuration.
188
+ """
189
+ pass
190
+
191
+ @abstractmethod
192
+ def get_model_by_name(self, model_name: str) -> Type:
193
+ """
194
+ Retrieve the model class based on a given model name (e.g. "app_label.ModelName").
195
+ """
196
+ pass
197
+
198
+ @abstractmethod
199
+ def get_model_name(
200
+ self, model: Union[Type[ORMModel], ORMModel]
201
+ ) -> str: # type:ignore
202
+ """
203
+ Retrieve the model name (e.g. "app_label.ModelName") for the given model class OR instance.
204
+ """
205
+ pass
206
+
207
+ @abstractmethod
208
+ def get_user(self, request: RequestType): # returns User
209
+ """
210
+ Get the request user.
211
+ """
212
+ pass
213
+
214
+ @abstractmethod
215
+ def build_model_graph(self, model: ORMModel) -> None: # type:ignore
216
+ """
217
+ Construct a graph representation of model relationships.
218
+ """
219
+ pass
220
+
221
+
222
+ class AbstractCustomQueryset(ABC):
223
+ @abstractmethod
224
+ def get_queryset(self, request: Optional[RequestType] = None) -> Any:
225
+ """
226
+ Return a custom queryset (e.g. a custom SQLAlchemy Query or Django QuerySet).
227
+
228
+ Args:
229
+ request: The current request object, which may contain user information
230
+
231
+ Returns:
232
+ A custom queryset
233
+ """
234
+ pass
235
+
236
+
237
+ class AbstractDataSerializer(ABC):
238
+ @abstractmethod
239
+ def serialize(
240
+ self,
241
+ data: Any,
242
+ model: ORMModel, # type:ignore
243
+ depth: int,
244
+ fields: Optional[Set[str]] = None,
245
+ allowed_fields: Optional[Dict[str, Set[str]]] = None,
246
+ ) -> dict:
247
+ """
248
+ Serialize the given data (a single instance or a list) for the specified model.
249
+ - `fields`: the set of field names requested by the client.
250
+ - `allowed_fields`: a mapping (by model name) of fields the user is permitted to access.
251
+
252
+ The effective fields are computed as the intersection of requested and allowed (if both are provided).
253
+ """
254
+ pass
255
+
256
+ @abstractmethod
257
+ def deserialize(
258
+ self,
259
+ model: ORMModel, # type:ignore
260
+ data: dict,
261
+ allowed_fields: Optional[Dict[str, Set[str]]] = None,
262
+ request: Optional[Any] = None,
263
+ ) -> dict:
264
+ """
265
+ Deserialize the input data into validated Python types for the specified model.
266
+ - `allowed_fields`: a mapping (by model name) of fields the user is allowed to edit.
267
+
268
+ Only keys that appear in the allowed set will be processed.
269
+ """
270
+ pass
271
+
272
+
273
+ class AbstractSchemaGenerator(ABC):
274
+ @abstractmethod
275
+ def generate_schema(
276
+ self,
277
+ model: ORMModel, # type:ignore
278
+ global_schema_overrides: Dict[ORMField, dict], # type:ignore
279
+ additional_fields: List[ORMField], # type:ignore
280
+ ) -> ModelSchemaMetadata:
281
+ """
282
+ Generate and return a schema for the given model.
283
+ Both global schema overrides and per-model additional fields are applied.
284
+ """
285
+ pass
286
+
287
+
288
+ class AbstractSchemaOverride(ABC):
289
+ @abstractmethod
290
+ def get_schema(self) -> Tuple[SchemaFieldMetadata, Dict[str, str], str]:
291
+ """
292
+ Return the schema for the field type.
293
+ """
294
+ pass
295
+
296
+
297
+ # --- Event Emitter ---
298
+ class AbstractEventEmitter(ABC):
299
+ @abstractmethod
300
+ def emit(
301
+ self, namespace: str, event_type: ActionType, data: Dict[str, Any]
302
+ ) -> None:
303
+ """
304
+ Emit an event to the specified namespace with the given event type and data.
305
+
306
+ Parameters:
307
+ -----------
308
+ namespace: str
309
+ The namespace/channel to emit the event to
310
+ event_type: ActionType
311
+ The type of event being emitted
312
+ data: Dict[str, Any]
313
+ The structured data payload to emit
314
+ """
315
+ pass
316
+
317
+ @abstractmethod
318
+ def has_permission(self, request: RequestType, namespace: str) -> bool:
319
+ """
320
+ Check if the given request has permission to access the channel identified by the namespace.
321
+ """
322
+ pass
323
+
324
+ @abstractmethod
325
+ def authenticate(self, request: RequestType) -> None:
326
+ """
327
+ Authenticate the request for the event emitter.
328
+ """
329
+ pass
330
+
331
+
332
+ # --- Permissions ---
333
+
334
+
335
+ class AbstractPermission(ABC):
336
+ @abstractmethod
337
+ def filter_queryset(
338
+ self, request: RequestType, queryset: ORMQuerySet
339
+ ) -> Any: # type:ignore
340
+ """
341
+ Given the request, queryset, and set of CRUD actions, return a queryset filtered according
342
+ to permission rules.
343
+ """
344
+ pass
345
+
346
+ @abstractmethod
347
+ def allowed_actions(
348
+ self, request: RequestType, model: ORMModel
349
+ ) -> Set[ActionType]: # type:ignore
350
+ """
351
+ Return the set of CRUD actions the user is permitted to perform on the model.
352
+ """
353
+ pass
354
+
355
+ @abstractmethod
356
+ def allowed_object_actions(
357
+ self, request: RequestType, obj: Any, model: ORMModel
358
+ ) -> Set[ActionType]: # type:ignore
359
+ """
360
+ Return the set of CRUD actions the user is permitted to perform on the specific object.
361
+ """
362
+ pass
363
+
364
+ def bulk_operation_allowed(
365
+ self,
366
+ request: RequestType,
367
+ items: ORMQuerySet,
368
+ action_type: ActionType,
369
+ model: type,
370
+ ) -> bool:
371
+ """
372
+ Default bulk permission check that simply loops over 'items'
373
+ and calls 'allowed_object_actions' on each one. If any item
374
+ fails, raise PermissionDenied.
375
+ """
376
+ for obj in items:
377
+ object_level_perms = self.allowed_object_actions(request, obj, model)
378
+ if action_type not in object_level_perms:
379
+ return False
380
+ return True
381
+
382
+ @abstractmethod
383
+ def visible_fields(
384
+ self, request: RequestType, model: ORMModel
385
+ ) -> Union[Set[str], Literal["__all__"]]: # type:ignore
386
+ """
387
+ Return the set of fields that are visible to the user for the given model and CRUD actions.
388
+ """
389
+ pass
390
+
391
+ @abstractmethod
392
+ def editable_fields(
393
+ self, request: RequestType, model: ORMModel
394
+ ) -> Union[Set[str], Literal["__all__"]]: # type:ignore
395
+ """
396
+ Return the set of fields that are editable by the user for the given model and CRUD actions.
397
+ """
398
+ pass
399
+
400
+ @abstractmethod
401
+ def create_fields(
402
+ self, request: RequestType, model: ORMModel
403
+ ) -> Union[Set[str], Literal["__all__"]]: # type:ignore
404
+ """
405
+ Return the set of fields that the user is allowed to specify in their create method
406
+ """
407
+ pass
408
+
409
+ class AbstractSearchProvider(ABC):
410
+ """Base class for search providers in StateZero."""
411
+
412
+ @abstractmethod
413
+ def search(self, queryset: ORMQuerySet, query: str, search_fields: Union[Set[str], Literal["__all__"]]) -> ORMQuerySet:
414
+ """
415
+ Apply search filtering to a queryset.
416
+
417
+ Args:
418
+ queryset: Django queryset
419
+ query: The search query string
420
+ search_fields: Set of field names to search in
421
+
422
+ Returns:
423
+ Filtered queryset with search applied
424
+ """
425
+ pass
426
+
427
+ class AbstractQueryOptimizer(ABC):
428
+ """
429
+ Abstract Base Class for query optimizers.
430
+
431
+ Defines the essential interface for optimizing a query object,
432
+ potentially using configuration provided during initialization.
433
+ """
434
+
435
+ def __init__(
436
+ self,
437
+ depth: Optional[int] = None,
438
+ fields_per_model: Optional[Dict[str, Set[str]]] = None,
439
+ get_model_name_func: Optional[Callable[[Type[ORMModel]], str]] = None
440
+ ):
441
+ """
442
+ Initializes the optimizer with common configuration potentially
443
+ used for generating optimization parameters if not provided directly
444
+ to the optimize method.
445
+
446
+ Args:
447
+ depth (Optional[int]): Default maximum relationship traversal depth
448
+ if generating field paths automatically.
449
+ fields_per_model (Optional[Dict[str, Set[str]]]): Default mapping of
450
+ model names (keys) to sets of required field/relationship names
451
+ (values), used if generating field paths automatically.
452
+ get_model_name_func (Optional[Callable]): Default function to get a
453
+ consistent string name for a model class, used with
454
+ fields_per_model if generating field paths automatically.
455
+ """
456
+ self.default_depth = depth
457
+ self.default_fields_per_model = fields_per_model
458
+ self.default_get_model_name_func = get_model_name_func
459
+ # Basic validation for depth if provided
460
+ if self.default_depth is not None and self.default_depth < 0:
461
+ raise ValueError("Depth cannot be negative.")
462
+
463
+ @abstractmethod
464
+ def optimize(
465
+ self,
466
+ queryset: Any,
467
+ fields: Optional[List[str]] = None,
468
+ **kwargs: Any
469
+ ) -> Any:
470
+ """
471
+ Optimizes the given query object.
472
+
473
+ Concrete implementations will use the provided queryset and potentially
474
+ the 'fields' list or the configuration from __init__ to apply
475
+ optimizations.
476
+
477
+ Args:
478
+ queryset (Any): The query object to optimize (e.g., a Django QuerySet).
479
+ fields (Optional[List[str]]): An explicit list of field paths to optimize for.
480
+ If provided, this typically overrides any
481
+ automatic path generation based on init config.
482
+ **kwargs: Additional optimization-specific parameters.
483
+
484
+ Returns:
485
+ Any: The optimized query object.
486
+
487
+ Raises:
488
+ NotImplementedError: If the concrete class doesn't implement this.
489
+ ValueError: If required parameters (like 'fields' or init config
490
+ for generation) are missing.
491
+ """
492
+ raise NotImplementedError
@@ -0,0 +1,184 @@
1
+ import logging
2
+ from typing import Any, Dict, Optional, Set, Type
3
+
4
+ from fastapi.encoders import jsonable_encoder
5
+
6
+ from statezero.core import AppConfig, ModelConfig, Registry
7
+ from statezero.core.ast_parser import ASTParser
8
+ from statezero.core.ast_validator import ASTValidator
9
+ from statezero.core.exceptions import PermissionDenied, ValidationError
10
+ from statezero.core.interfaces import (AbstractDataSerializer,
11
+ AbstractORMProvider,
12
+ AbstractSchemaGenerator)
13
+ from statezero.core.types import ActionType
14
+
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.DEBUG)
17
+
18
+
19
+ def _filter_writable_data(
20
+ data: Dict[str, Any],
21
+ req: Any,
22
+ model: Type,
23
+ model_config: ModelConfig,
24
+ orm_provider: AbstractORMProvider,
25
+ create: bool = False,
26
+ ) -> Dict[str, Any]:
27
+ """
28
+ Filter out keys for which the user does not have write permission.
29
+ When `create` is True, use the permission's `create_fields` method;
30
+ otherwise, use `editable_fields`.
31
+
32
+ If the allowed fields set contains "__all__", return the original data.
33
+ """
34
+ all_fields = orm_provider.get_fields(model)
35
+ allowed_fields: Set[str] = set()
36
+
37
+ for permission_cls in model_config.permissions:
38
+ if create:
39
+ permission_fields = permission_cls().create_fields(req, model)
40
+ else:
41
+ permission_fields = permission_cls().editable_fields(req, model)
42
+ # handle the __all__ shorthand
43
+ if permission_fields == "__all__":
44
+ permission_fields = all_fields
45
+ else:
46
+ permission_fields &= all_fields
47
+ allowed_fields |= permission_fields
48
+
49
+ return {k: v for k, v in data.items() if k in allowed_fields}
50
+
51
+
52
+ class RequestProcessor:
53
+ def __init__(
54
+ self,
55
+ config: AppConfig,
56
+ registry: Registry,
57
+ orm_provider: AbstractORMProvider = None,
58
+ data_serializer: AbstractDataSerializer = None,
59
+ schema_generator: AbstractSchemaGenerator = None,
60
+ schema_overrides: Dict = None,
61
+ ):
62
+ self.orm_provider = orm_provider or config.orm_provider
63
+ self.data_serializer = data_serializer or config.serializer
64
+ self.schema_generator = schema_generator or config.schema_generator
65
+ self.schema_overrides = schema_overrides or config.schema_overrides
66
+ self.registry = registry
67
+ self.config = config
68
+
69
+ def process_schema(self, req: Any) -> Dict[str, Any]:
70
+ try:
71
+ model_name: str = req.parser_context.get("kwargs", {}).get("model_name")
72
+ model = self.orm_provider.get_model_by_name(model_name)
73
+ config: ModelConfig = self.registry.get_config(model)
74
+
75
+ # In production, check that the user has permission to at least one of the CRUD actions.
76
+ if not self.config.DEBUG:
77
+ allowed_actions: Set[ActionType] = set()
78
+ for permission_cls in config.permissions:
79
+ allowed_actions |= permission_cls().allowed_actions(req, model)
80
+ required_actions = {
81
+ ActionType.CREATE,
82
+ ActionType.READ,
83
+ ActionType.UPDATE,
84
+ ActionType.DELETE,
85
+ }
86
+ if allowed_actions.isdisjoint(required_actions):
87
+ raise PermissionDenied(
88
+ "User does not have any permissions required to access the schema."
89
+ )
90
+
91
+ schema_meta = self.schema_generator.generate_schema(
92
+ model=model,
93
+ global_schema_overrides=self.schema_overrides,
94
+ additional_fields=config.additional_fields,
95
+ )
96
+ schema_dict = schema_meta.model_dump()
97
+ return jsonable_encoder(schema_dict)
98
+ except Exception as e:
99
+ logger.exception("Error in process_schema")
100
+ raise ValidationError(str(e))
101
+
102
+ def process_request(self, req: Any) -> Dict[str, Any]:
103
+ body: Dict[str, Any] = req.data or {}
104
+ ast_body: Dict[str, Any] = body.get("ast", {})
105
+ initial_query_ast: Dict[str, Any] = ast_body.get("initial_query", {})
106
+ final_query_ast: Dict[str, Any] = ast_body.get("query", {})
107
+
108
+ model_name: str = req.parser_context.get("kwargs", {}).get("model_name")
109
+ model = self.orm_provider.get_model_by_name(model_name)
110
+ model_config: ModelConfig = self.registry.get_config(model)
111
+
112
+ base_queryset = self.orm_provider.get_queryset(
113
+ req=req,
114
+ model=model,
115
+ initial_ast=initial_query_ast,
116
+ custom_querysets=model_config.custom_querysets,
117
+ registered_permissions=model_config.permissions,
118
+ )
119
+
120
+ for permission_cls in model_config.permissions:
121
+ base_queryset = permission_cls().filter_queryset(req, base_queryset)
122
+
123
+ # ---- PERMISSION CHECKS: Global Level (Write operations remain here) ----
124
+ requested_actions: Set[ActionType] = ASTParser.get_requested_action_types(
125
+ final_query_ast
126
+ )
127
+
128
+ allowed_global_actions: Set[ActionType] = set()
129
+ for permission_cls in model_config.permissions:
130
+ allowed_global_actions |= permission_cls().allowed_actions(req, model)
131
+ if "__all__" not in allowed_global_actions:
132
+ if not requested_actions.issubset(allowed_global_actions):
133
+ missing = requested_actions - allowed_global_actions
134
+ missing_str = ", ".join(action.value for action in missing)
135
+ raise PermissionDenied(
136
+ f"Missing global permissions for actions: {missing_str}"
137
+ )
138
+
139
+ # For READ operations, delegate field permission checks to ASTValidator.
140
+ serializer_options = ast_body.get("serializerOptions", {})
141
+
142
+ # Invoke the ASTValidator to check read field permissions.
143
+ model_graph = self.orm_provider.build_model_graph(model)
144
+ validator = ASTValidator(
145
+ model_graph=model_graph,
146
+ get_model_name=self.orm_provider.get_model_name,
147
+ registry=self.registry,
148
+ request=req,
149
+ get_model_by_name=self.orm_provider.get_model_by_name,
150
+ )
151
+ validator.validate_fields(final_query_ast, model)
152
+
153
+ # ---- WRITE OPERATIONS: Filter incoming data to include only writable fields. ----
154
+ op = final_query_ast.get("type")
155
+ if op in ["create", "update"]:
156
+ data = final_query_ast.get("data", {})
157
+ # For create operations, pass create=True so that create_fields are used.
158
+ filtered_data = _filter_writable_data(
159
+ data, req, model, model_config, self.orm_provider, create=(op == "create")
160
+ )
161
+ final_query_ast["data"] = filtered_data
162
+ elif op in ["get_or_create", "update_or_create"]:
163
+ if "lookup" in final_query_ast:
164
+ final_query_ast["lookup"] = _filter_writable_data(
165
+ final_query_ast["lookup"], req, model, model_config, self.orm_provider, create=True
166
+ )
167
+ if "defaults" in final_query_ast:
168
+ final_query_ast["defaults"] = _filter_writable_data(
169
+ final_query_ast["defaults"], req, model, model_config, self.orm_provider, create=True
170
+ )
171
+
172
+ # Create and use the AST parser directly, instead of delegating to ORM provider
173
+ self.orm_provider.set_queryset(base_queryset)
174
+ parser = ASTParser(
175
+ engine=self.orm_provider,
176
+ serializer=self.data_serializer,
177
+ model=model,
178
+ config=self.config,
179
+ registry=self.registry,
180
+ serializer_options=serializer_options or {},
181
+ request=req,
182
+ )
183
+ result: Dict[str, Any] = parser.parse(final_query_ast)
184
+ return result