statezero 0.1.0b16__py3-none-any.whl → 0.1.0b18__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.

Potentially problematic release.


This version of statezero might be problematic. Click here for more details.

@@ -763,6 +763,7 @@ class DjangoORMAdapter(AbstractORMProvider):
763
763
  def get_fields(self, model: models.Model) -> Set[str]:
764
764
  """
765
765
  Return a set of the model fields.
766
+ Includes both database fields and additional_fields (computed fields).
766
767
  """
767
768
  model_config = registry.get_config(model)
768
769
  if model_config.fields and "__all__" != model_config.fields:
@@ -775,6 +776,14 @@ class DjangoORMAdapter(AbstractORMProvider):
775
776
  resolved_fields = resolved_fields.union(additional_fields)
776
777
  return resolved_fields
777
778
 
779
+ def get_db_fields(self, model: models.Model) -> Set[str]:
780
+ """
781
+ Return only actual database fields for the model.
782
+ Excludes read-only additional_fields (computed fields).
783
+ Used for deserialization - hooks can write to any DB field.
784
+ """
785
+ return set(field.name for field in model._meta.get_fields())
786
+
778
787
  def build_model_graph(
779
788
  self, model: Type[models.Model], model_graph: nx.DiGraph = None
780
789
  ) -> nx.DiGraph:
@@ -41,9 +41,9 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
41
41
  all_field_names: Set[str] = set()
42
42
  db_field_names: Set[str] = set()
43
43
 
44
- if model_config.frontend_fields != "__all__":
44
+ if model_config.fields != "__all__":
45
45
  all_fields = [
46
- field for field in all_fields if field.name in model_config.frontend_fields
46
+ field for field in all_fields if field.name in model_config.fields
47
47
  ]
48
48
 
49
49
  for field in all_fields:
@@ -323,8 +323,7 @@ class DjangoSchemaGenerator(AbstractSchemaGenerator):
323
323
  @staticmethod
324
324
  def is_field_required(field: models.Field) -> bool:
325
325
  return (
326
- not field.blank
327
- and not field.null
326
+ not field.null
328
327
  and field.default == models.fields.NOT_PROVIDED
329
328
  )
330
329
 
@@ -6,7 +6,7 @@ from rest_framework import serializers
6
6
  import contextvars
7
7
  from contextlib import contextmanager
8
8
  import logging
9
- from cytoolz import pluck
9
+ from cytoolz import pluck, keyfilter
10
10
  from zen_queries import queries_disabled
11
11
 
12
12
  from statezero.adaptors.django.config import config, registry
@@ -484,33 +484,42 @@ class DRFDynamicSerializer(AbstractDataSerializer):
484
484
  # Serious security issue if fields_map is None
485
485
  assert fields_map is not None, "fields_map is required and cannot be None"
486
486
 
487
- # Use the context manager for the duration of deserialization
488
- with fields_map_context(fields_map):
489
- # Create serializer class
490
- serializer_class = DynamicModelSerializer.for_model(model)
491
- available_fields = set(serializer_class().fields.keys())
487
+ # Get model name and allowed fields from fields_map
488
+ model_name = config.orm_provider.get_model_name(model)
489
+ allowed_fields = fields_map.get(model_name, set())
492
490
 
493
- try:
494
- model_config = registry.get_config(model)
495
- if model_config.pre_hooks:
496
- for hook in model_config.pre_hooks:
497
- hook_result = hook(data, request=request)
498
- if settings.DEBUG:
499
- data = _check_pre_hook_result(
500
- original_data=data,
501
- result_data=hook_result,
502
- model=model,
503
- serializer_fields=available_fields
504
- )
505
- else:
506
- data = hook_result or data
507
- except ValueError:
508
- # No model config available
509
- model_config = None
491
+ # Filter user input to only allowed fields (security boundary)
492
+ data = dict(keyfilter(lambda k: k in allowed_fields, data))
493
+
494
+ try:
495
+ model_config = registry.get_config(model)
496
+ except ValueError:
497
+ # No model config available
498
+ model_config = None
499
+
500
+ # Run pre-hooks on filtered data (hooks can add any DB fields)
501
+ if model_config and model_config.pre_hooks:
502
+ for hook in model_config.pre_hooks:
503
+ hook_result = hook(data, request=request)
504
+ if settings.DEBUG:
505
+ # Note: available_fields check removed since hooks can add any DB field
506
+ data = hook_result or data
507
+ else:
508
+ data = hook_result or data
509
+
510
+ # Expand fields_map to all DB fields for serializer validation
511
+ # This allows hooks to add fields that aren't in the original fields_map
512
+ all_db_fields = config.orm_provider.get_db_fields(model)
513
+ expanded_fields_map = {model_name: all_db_fields}
514
+
515
+ # Use the context manager with expanded fields map
516
+ with fields_map_context(expanded_fields_map):
517
+ # Create serializer class with all DB fields available
518
+ serializer_class = DynamicModelSerializer.for_model(model)
510
519
 
511
520
  # Create serializer
512
521
  serializer = serializer_class(
513
- data=data,
522
+ data=data,
514
523
  partial=partial,
515
524
  request=request
516
525
  )
@@ -1,6 +1,6 @@
1
1
  from django.urls import path
2
2
 
3
- from .views import EventsAuthView, ModelListView, ModelView, SchemaView, FileUploadView, FastUploadView, ActionSchemaView, ActionView, ValidateView
3
+ from .views import EventsAuthView, ModelListView, ModelView, SchemaView, FileUploadView, FastUploadView, ActionSchemaView, ActionView, ValidateView, FieldPermissionsView
4
4
 
5
5
  app_name = "statezero"
6
6
 
@@ -12,6 +12,7 @@ urlpatterns = [
12
12
  path("actions/<str:action_name>/", ActionView.as_view(), name="action"),
13
13
  path("actions-schema/", ActionSchemaView.as_view(), name="actions_schema"),
14
14
  path("<str:model_name>/validate/", ValidateView.as_view(), name="validate"),
15
+ path("<str:model_name>/field-permissions/", FieldPermissionsView.as_view(), name="field_permissions"),
15
16
  path("<str:model_name>/get-schema/", SchemaView.as_view(), name="schema_view"),
16
17
  path("<str:model_name>/", ModelView.as_view(), name="model_view"),
17
18
  ]
@@ -485,3 +485,104 @@ class ValidateView(APIView):
485
485
  except Exception as original_exception:
486
486
  # Let StateZero's exception handler deal with ValidationError, PermissionDenied, etc.
487
487
  return explicit_exception_handler(original_exception)
488
+
489
+
490
+ class FieldPermissionsView(APIView):
491
+ """
492
+ Returns user-specific field permissions for a given model.
493
+ Used by frontend forms to determine which fields to show/enable at runtime.
494
+ """
495
+
496
+ permission_classes = [permission_class]
497
+
498
+ def get(self, request, model_name):
499
+ """Get field permissions for the current user."""
500
+ try:
501
+ # Create processor following the same pattern as other views
502
+ processor = RequestProcessor(config=config, registry=registry)
503
+
504
+ # Get model using the processor's ORM provider
505
+ try:
506
+ model = processor.orm_provider.get_model_by_name(model_name)
507
+ except (LookupError, ValueError):
508
+ return Response({"error": f"Model {model_name} not found"}, status=404)
509
+
510
+ if not model:
511
+ return Response({"error": f"Model {model_name} not found"}, status=404)
512
+
513
+ try:
514
+ model_config = processor.registry.get_config(model)
515
+ except ValueError:
516
+ return Response(
517
+ {"error": f"Model {model_name} not registered"}, status=404
518
+ )
519
+
520
+ # Compute field permissions using the same logic as ASTParser._get_operation_fields
521
+ all_fields = processor.orm_provider.get_fields(model)
522
+
523
+ visible_fields = self._compute_operation_fields(
524
+ model, model_config, all_fields, request, "read"
525
+ )
526
+ creatable_fields = self._compute_operation_fields(
527
+ model, model_config, all_fields, request, "create"
528
+ )
529
+ editable_fields = self._compute_operation_fields(
530
+ model, model_config, all_fields, request, "update"
531
+ )
532
+
533
+ return Response(
534
+ {
535
+ "visible_fields": list(visible_fields),
536
+ "creatable_fields": list(creatable_fields),
537
+ "editable_fields": list(editable_fields),
538
+ },
539
+ status=200,
540
+ )
541
+
542
+ except Exception as original_exception:
543
+ # Let StateZero's exception handler deal with errors
544
+ return explicit_exception_handler(original_exception)
545
+
546
+ def _compute_operation_fields(self, model, model_config, all_fields, request, operation_type):
547
+ """
548
+ Compute allowed fields for a specific operation type.
549
+ Replicates the logic from ASTParser._get_operation_fields.
550
+ """
551
+ from typing import Union, Set, Literal
552
+
553
+ allowed_fields = set()
554
+
555
+ for permission_cls in model_config.permissions:
556
+ permission = permission_cls()
557
+
558
+ # Get the appropriate field set based on operation
559
+ if operation_type == "read":
560
+ fields = permission.visible_fields(request, model)
561
+ elif operation_type == "create":
562
+ fields = permission.create_fields(request, model)
563
+ elif operation_type == "update":
564
+ fields = permission.editable_fields(request, model)
565
+ else:
566
+ fields = set()
567
+
568
+ # If any permission allows all fields
569
+ if fields == "__all__":
570
+ # For read operations, default "__all__" to frontend_fields
571
+ if operation_type == "read":
572
+ # If frontend_fields is also "__all__", then return all fields
573
+ if model_config.fields == "__all__":
574
+ return all_fields
575
+ # Otherwise, use frontend_fields as the default for "__all__"
576
+ else:
577
+ fields = model_config.fields
578
+ fields &= all_fields # Ensure fields actually exist
579
+ allowed_fields |= fields
580
+ else:
581
+ # For create/update operations, "__all__" means truly all fields
582
+ return all_fields
583
+ else:
584
+ # Add allowed fields from this permission
585
+ fields &= all_fields # Ensure fields actually exist
586
+ allowed_fields |= fields
587
+
588
+ return allowed_fields
@@ -386,19 +386,8 @@ class ASTParser:
386
386
 
387
387
  # If any permission allows all fields
388
388
  if fields == "__all__":
389
- # NEW: For read operations, default "__all__" to frontend_fields
390
- if operation_type == "read":
391
- # If frontend_fields is also "__all__", then return all fields
392
- if model_config.frontend_fields == "__all__":
393
- return all_fields
394
- # Otherwise, use frontend_fields as the default for "__all__"
395
- else:
396
- fields = model_config.frontend_fields
397
- fields &= all_fields # Ensure fields actually exist
398
- allowed_fields |= fields
399
- else:
400
- # For create/update operations, "__all__" means truly all fields
401
- return all_fields
389
+ return all_fields
390
+
402
391
  # Add allowed fields from this permission
403
392
  else: # Ensure we're not operating on the string "__all__"
404
393
  fields &= all_fields # Ensure fields actually exist
statezero/core/config.py CHANGED
@@ -181,7 +181,6 @@ class ModelConfig:
181
181
  searchable_fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
182
182
  ordering_fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
183
183
  fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
184
- frontend_fields: Optional[Union[Set[str], Literal["__all__"]]] = None,
185
184
  display: Optional[Any] = None,
186
185
  DEBUG: bool = False,
187
186
  ):
@@ -196,7 +195,6 @@ class ModelConfig:
196
195
  self.searchable_fields = searchable_fields or set()
197
196
  self.ordering_fields = ordering_fields or set()
198
197
  self.fields = fields or "__all__"
199
- self.frontend_fields = frontend_fields or self.fields
200
198
  self.display = display
201
199
  self.DEBUG = DEBUG or False
202
200
 
@@ -70,6 +70,16 @@ class AbstractORMProvider(ABC):
70
70
  def get_fields(self, model: ORMModel) -> Set[str]:
71
71
  """
72
72
  Get all of the model fields - doesn't apply permissions check.
73
+ Includes both database fields and additional_fields (computed fields).
74
+ """
75
+ pass
76
+
77
+ @abstractmethod
78
+ def get_db_fields(self, model: ORMModel) -> Set[str]:
79
+ """
80
+ Get only the actual database fields for a model.
81
+ Excludes read-only additional_fields (computed fields).
82
+ Used for deserialization - hooks can write to any DB field.
73
83
  """
74
84
  pass
75
85
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statezero
3
- Version: 0.1.0b16
3
+ Version: 0.1.0b18
4
4
  Summary: Connect your Python backend to a modern JavaScript SPA frontend with 90% less complexity.
5
5
  Author-email: Robert <robert.herring@statezero.dev>
6
6
  Project-URL: homepage, https://www.statezero.dev
@@ -10,13 +10,13 @@ statezero/adaptors/django/exception_handler.py,sha256=SHIg-A1hiMql1pb026DxkUijUU
10
10
  statezero/adaptors/django/f_handler.py,sha256=yvITFj9UAnz8-r-aLEcWoz48tBhZ08-VMq9Fsm2uiN8,12305
11
11
  statezero/adaptors/django/helpers.py,sha256=0Dyq5vboDuTUaH-KpS3oVDjastA9yv6xI6XpBuvRM3I,5974
12
12
  statezero/adaptors/django/middleware.py,sha256=YVr8fkqCk51xJQM-ovtrUiB9Kt9H81cLd9xv4cM9YlM,410
13
- statezero/adaptors/django/orm.py,sha256=Z62XheCvuKIpKOoIIiLLOwrpJ5jPhv-BGxg-pqPgNaU,40757
13
+ statezero/adaptors/django/orm.py,sha256=NjxZQDI0r5Ccl5bPxFcPxnao2oJlN0k5_RdGs4kCWAo,41191
14
14
  statezero/adaptors/django/permissions.py,sha256=fU2c4bKK0zX2uuVB0UazZHTI-5OkiI5-BtPNcPEWmW0,9525
15
15
  statezero/adaptors/django/query_optimizer.py,sha256=-iAh5kyE8WNZdjb_qBbNag_nxKzejroUYPBdwG_uVaQ,41462
16
- statezero/adaptors/django/schemas.py,sha256=E-crPopLIFpcCVE31d39KAv1r_B0XNS7iCR09WjFvGo,14091
17
- statezero/adaptors/django/serializers.py,sha256=YFFDu6bzoWkSEOVH5Wmc4yJ8SaOkUA6HbXXYt6djlfc,23296
18
- statezero/adaptors/django/urls.py,sha256=OrGQ60vj_wrbiREAKmYDZTwohpKmgjH9n0fdOw1qPaY,924
19
- statezero/adaptors/django/views.py,sha256=2bJDbXuRGoG2_7zyapWzmRzpSVUHkCpcI58wsrXN1jc,19947
16
+ statezero/adaptors/django/schemas.py,sha256=cnDzfUWS_oH16kEc0cy6h7tNmBSPpNp_Rz3sH0vU2Ak,14040
17
+ statezero/adaptors/django/serializers.py,sha256=lvcO4Vn9UM8TD28pse1XxrQugkFlzSeL8rYDHRU5bpk,23722
18
+ statezero/adaptors/django/urls.py,sha256=bLn_kL5a5VBQfhl2-UCpLmguSenjJ7bouPoKMKNTX5M,1054
19
+ statezero/adaptors/django/views.py,sha256=rALVs8McRzgf-YhLoq7GqisjKiLxaq2k0WUrcGhmRvA,24183
20
20
  statezero/adaptors/django/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  statezero/adaptors/django/extensions/custom_field_serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py,sha256=BaOaJPmyzCp-YFwpsTOvGHjHpk6s8UJuZ5JsF-PEGV4,4518
@@ -29,20 +29,20 @@ statezero/adaptors/django/search_providers/basic_search.py,sha256=5_GJ1r_B6JdIYX
29
29
  statezero/adaptors/django/search_providers/postgres_search.py,sha256=IMoHxzfi-Y3hAxPND4Xc6GatrPs1eXAqpmcfwt5Zr14,2459
30
30
  statezero/core/__init__.py,sha256=Z6RTutAAElLMEjBFphVVmpySPdJBA55j-Uo0BtR7c5E,1040
31
31
  statezero/core/actions.py,sha256=eq4zuDhK1h-nZ24jUQhiWL6BcMqq-W4BhdVNdYegymw,2969
32
- statezero/core/ast_parser.py,sha256=QTFRDwanN2jJJOs4SeRdRPiVZWlK-tkmwZBlQNByew0,39321
32
+ statezero/core/ast_parser.py,sha256=QHpwWlVQTrxE1xen4xMZcALDn7eEl4CL5PoVfGQg_c4,38539
33
33
  statezero/core/ast_validator.py,sha256=YZAflPyba0kXWBNhd1Z_XeEk-_zUzM6MkY9zSlX1PMs,11582
34
34
  statezero/core/classes.py,sha256=TlJYUhiYniTJqZCSVo_-85mgJ4muhCPpJWxlgG-Vph8,6996
35
- statezero/core/config.py,sha256=kOcQPzBA06d8EliP2bVY0daFlt8bm-8ZOsqb5x7-8JA,11822
35
+ statezero/core/config.py,sha256=RNRujZg393a2B_uj4isUUIhwEUZFAfSlafKHu_O3EAs,11679
36
36
  statezero/core/context_storage.py,sha256=DVx525ZMRorj41kg5K0N6pPdGkQ5_XEJcBucpH5ChxQ,162
37
37
  statezero/core/event_bus.py,sha256=2IFLBHSkLzpm1AX0MfSXSmF2X-lXK-gOoODZCtB2Jdw,6284
38
38
  statezero/core/event_emitters.py,sha256=qjMbeUmdn4bG7WiVfqTmNdaflEea5amnTEpOn5X0J44,2046
39
39
  statezero/core/exceptions.py,sha256=_krMHWW9qBbMXvvqFdWf85a3Kayn7XbJczfC3x3gmBI,3330
40
40
  statezero/core/hook_checks.py,sha256=uqtvwRx1qGsF7Vc49elAWdOjMzhuv3RADKY1wiLvhK4,3425
41
- statezero/core/interfaces.py,sha256=kVkNWyh52tUlzD02CRheLJof3DyQoVcPuvX33fL6sn8,20544
41
+ statezero/core/interfaces.py,sha256=YgONfBl8PY0sLLILMMmBAJ84xJS-rNCVjhXtsCzoOAE,20938
42
42
  statezero/core/process_request.py,sha256=dwIeBEVOE8zA-oE1h65XNOGiVqFbbXA7SzTAguLNgZk,8060
43
43
  statezero/core/types.py,sha256=mMtqK3fGhEM6LtzUgQrxlyP-V0VgVqc-1eVKgRjTzp0,913
44
- statezero-0.1.0b16.dist-info/licenses/license.md,sha256=0uKjybTt9K_YbEmYgf25JN292qjjJ-BPofvIZ3wdtX4,7411
45
- statezero-0.1.0b16.dist-info/METADATA,sha256=Iwy9H2FRv6eeWVch1qdPCS_-XCq1_rCZW0aQpZEl15c,6704
46
- statezero-0.1.0b16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- statezero-0.1.0b16.dist-info/top_level.txt,sha256=UAuZYPKczradU1kcMQxsGjUzEW0qdgsqzhXyscrcLpw,10
48
- statezero-0.1.0b16.dist-info/RECORD,,
44
+ statezero-0.1.0b18.dist-info/licenses/license.md,sha256=0uKjybTt9K_YbEmYgf25JN292qjjJ-BPofvIZ3wdtX4,7411
45
+ statezero-0.1.0b18.dist-info/METADATA,sha256=XIu8HuwWqs4D4Iq-nCRr8mIPG-hlSxAa_2E9w2zCmvE,6704
46
+ statezero-0.1.0b18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ statezero-0.1.0b18.dist-info/top_level.txt,sha256=UAuZYPKczradU1kcMQxsGjUzEW0qdgsqzhXyscrcLpw,10
48
+ statezero-0.1.0b18.dist-info/RECORD,,