Server-driven invalidation + raw context response format
Mutation responses now include invalidation directives from the server:
POST /api/mizan/call/
→ {"result": {...}, "invalidate": ["user"]}
The client never hardcodes invalidation targets. The server resolves
affects= metadata and returns what to invalidate. mizan-runtime reads
the invalidate key and triggers refetches automatically.
Context fetch returns raw bundled data (not wrapped):
GET /api/mizan/ctx/user/?user_id=5
→ {"user_profile": {...}, "user_orders": [...]}
Also fixed QueryDict handling (use .dict() not dict() to avoid
list-wrapped values).
267 Django tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -478,7 +478,25 @@ def function_call_view(request: HttpRequest) -> JsonResponse:
|
||||
}.get(result.code, 400)
|
||||
return result.to_response(status=status)
|
||||
|
||||
return result.to_response()
|
||||
# Build response with server-driven invalidation
|
||||
view_class = get_function(fn_name)
|
||||
response_data = {"result": result.data}
|
||||
|
||||
if view_class:
|
||||
meta = getattr(view_class, "_meta", {})
|
||||
affects = meta.get("affects")
|
||||
if affects:
|
||||
invalidate = []
|
||||
for target in affects:
|
||||
if target["type"] == "context":
|
||||
invalidate.append(target["name"])
|
||||
elif target["type"] == "function" and target.get("context"):
|
||||
invalidate.append(target["context"])
|
||||
if invalidate:
|
||||
# Dedupe while preserving order
|
||||
response_data["invalidate"] = list(dict.fromkeys(invalidate))
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
def execute_context(
|
||||
@@ -560,14 +578,11 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
||||
|
||||
Endpoint: GET /api/mizan/ctx/<context_name>/?param1=val1¶m2=val2
|
||||
|
||||
Response on success:
|
||||
Response: raw bundled data (keys are function names, values are results)
|
||||
{
|
||||
"error": false,
|
||||
"data": {
|
||||
"user_profile": { ... },
|
||||
"user_orders": [ ... ]
|
||||
}
|
||||
}
|
||||
"""
|
||||
if request.method != "GET":
|
||||
return FunctionError(
|
||||
@@ -575,7 +590,7 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
||||
message="Only GET method allowed",
|
||||
).to_response(status=405)
|
||||
|
||||
params = dict(request.GET)
|
||||
params = request.GET.dict()
|
||||
result = execute_context(request, context_name, params)
|
||||
|
||||
if isinstance(result, FunctionError):
|
||||
@@ -590,4 +605,5 @@ def context_fetch_view(request: HttpRequest, context_name: str) -> JsonResponse:
|
||||
}.get(result.code, 400)
|
||||
return result.to_response(status=status)
|
||||
|
||||
return result.to_response()
|
||||
# Return raw bundled data (not wrapped in {"error": false, "data": ...})
|
||||
return JsonResponse(result.data)
|
||||
|
||||
@@ -756,6 +756,97 @@ class AffectsTests(TestCase):
|
||||
self.assertNotIn("affects", plain._meta)
|
||||
|
||||
|
||||
class ServerDrivenInvalidationTests(TestCase):
|
||||
"""Tests that mutation responses include invalidation directives."""
|
||||
|
||||
def setUp(self):
|
||||
clear_registry()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def tearDown(self):
|
||||
clear_registry()
|
||||
|
||||
def test_mutation_response_includes_invalidate(self):
|
||||
"""Mutation with affects= returns invalidate in response."""
|
||||
from mizan.client.executor import function_call_view
|
||||
|
||||
UserCtx = ReactContext("user")
|
||||
|
||||
@client(context=UserCtx)
|
||||
def user_profile(request: HttpRequest) -> ValidOutput:
|
||||
return ValidOutput(valid=True)
|
||||
|
||||
@client(affects=UserCtx)
|
||||
def edit_profile(request: HttpRequest, name: str) -> ValidOutput:
|
||||
return ValidOutput(valid=True)
|
||||
|
||||
register(user_profile, "user_profile")
|
||||
register(edit_profile, "edit_profile")
|
||||
|
||||
request = self.factory.post(
|
||||
"/api/mizan/call/",
|
||||
json.dumps({"fn": "edit_profile", "args": {"name": "Ryth"}}),
|
||||
content_type="application/json",
|
||||
)
|
||||
request.user = AnonymousUser()
|
||||
request._dont_enforce_csrf_checks = True
|
||||
|
||||
response = function_call_view(request)
|
||||
data = json.loads(response.content)
|
||||
|
||||
self.assertIn("result", data)
|
||||
self.assertIn("invalidate", data)
|
||||
self.assertEqual(data["invalidate"], ["user"])
|
||||
|
||||
def test_mutation_without_affects_has_no_invalidate(self):
|
||||
"""Mutation without affects= returns result only."""
|
||||
from mizan.client.executor import function_call_view
|
||||
|
||||
@client
|
||||
def plain_action(request: HttpRequest) -> ValidOutput:
|
||||
return ValidOutput(valid=True)
|
||||
|
||||
register(plain_action, "plain_action")
|
||||
|
||||
request = self.factory.post(
|
||||
"/api/mizan/call/",
|
||||
json.dumps({"fn": "plain_action", "args": {}}),
|
||||
content_type="application/json",
|
||||
)
|
||||
request.user = AnonymousUser()
|
||||
request._dont_enforce_csrf_checks = True
|
||||
|
||||
response = function_call_view(request)
|
||||
data = json.loads(response.content)
|
||||
|
||||
self.assertIn("result", data)
|
||||
self.assertNotIn("invalidate", data)
|
||||
|
||||
def test_context_fetch_returns_raw_data(self):
|
||||
"""Context GET returns raw bundled data, not wrapped."""
|
||||
from mizan.client.executor import context_fetch_view
|
||||
|
||||
class Out(BaseModel):
|
||||
name: str
|
||||
|
||||
@client(context="team")
|
||||
def team_info(request: HttpRequest, team_id: int) -> Out:
|
||||
return Out(name=f"team_{team_id}")
|
||||
|
||||
register(team_info, "team_info")
|
||||
|
||||
request = self.factory.get("/api/mizan/ctx/team/?team_id=3")
|
||||
request.user = AnonymousUser()
|
||||
|
||||
response = context_fetch_view(request, "team")
|
||||
data = json.loads(response.content)
|
||||
|
||||
# Raw data — no "error" or "data" wrapper
|
||||
self.assertNotIn("error", data)
|
||||
self.assertIn("team_info", data)
|
||||
self.assertEqual(data["team_info"]["name"], "team_3")
|
||||
|
||||
|
||||
class ContextFetchTests(TestCase):
|
||||
"""Tests for the bundled context fetch endpoint (execute_context)."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user