From fa7fe1a0431fffe12b70847a1d62acbc80bc6d98 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sat, 1 Nov 2025 19:04:26 +0530 Subject: [PATCH 1/7] add cache extension --- backend/apps/common/extensions.py | 48 +++++++++++++++++++++++++++++++ backend/settings/base.py | 2 ++ 2 files changed, 50 insertions(+) create mode 100644 backend/apps/common/extensions.py diff --git a/backend/apps/common/extensions.py b/backend/apps/common/extensions.py new file mode 100644 index 0000000000..dd8245a57a --- /dev/null +++ b/backend/apps/common/extensions.py @@ -0,0 +1,48 @@ +"""Strawberry extensions.""" + +import json + +from django.conf import settings +from django.core.cache import cache +from strawberry.extensions.field_extension import FieldExtension + + +class CacheFieldExtension(FieldExtension): + """Cache FieldExtension class.""" + + def __init__(self, cache_timeout: int | None = None, prefix: str | None = None): + """Initialize the cache extension. + + Args: + cache_timeout (int | None): The TTL for cache entries in seconds. + prefix (str | None): A prefix for the cache key. + + """ + self.cache_timeout = cache_timeout or settings.GRAPHQL_RESOLVER_CACHE_TIME_SECONDS + self.prefix = prefix or settings.GRAPHQL_RESOLVER_CACHE_PREFIX + + def generate_key(self, info, kwargs: dict) -> str: + """Generate a unique cache key for a field. + + Args: + info (Info): The Strawberry execution info. + kwargs (dict): The resolver's arguments. + + Returns: + str: The unique cache key. + + """ + args_str = json.dumps(kwargs, sort_keys=True) + + return f"{self.prefix}:{info.path.typename}:{info.path.key}:{args_str}" + + def resolve(self, next_, source, info, **kwargs): + """Wrap the resolver to provide caching.""" + cache_key = self.generate_key(info, kwargs) + if cached_result := cache.get(cache_key): + return cached_result + + if result := next_(source, info, **kwargs): + cache.set(cache_key, result, self.cache_timeout) + + return result diff --git a/backend/settings/base.py b/backend/settings/base.py index 2ae07655cf..19b03a84bf 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -128,6 +128,8 @@ class Base(Configuration): API_PAGE_SIZE = 100 API_CACHE_PREFIX = "api-response" API_CACHE_TIME_SECONDS = 86400 # 24 hours. + GRAPHQL_RESOLVER_CACHE_PREFIX = "graphql-resolver" + GRAPHQL_RESOLVER_CACHE_TIME_SECONDS = 86400 # 24 hours. NINJA_PAGINATION_CLASS = "apps.api.rest.v0.pagination.CustomPagination" NINJA_PAGINATION_PER_PAGE = API_PAGE_SIZE From 8826228752be0d48e94179cc844a3d9333458d25 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sat, 1 Nov 2025 19:24:23 +0530 Subject: [PATCH 2/7] add tests for cache extension --- backend/tests/apps/common/extensions_test.py | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 backend/tests/apps/common/extensions_test.py diff --git a/backend/tests/apps/common/extensions_test.py b/backend/tests/apps/common/extensions_test.py new file mode 100644 index 0000000000..eb0e6a950b --- /dev/null +++ b/backend/tests/apps/common/extensions_test.py @@ -0,0 +1,101 @@ +from unittest.mock import MagicMock, patch + +import pytest +from strawberry.types.info import Info + +from apps.common.extensions import CacheFieldExtension + + +@pytest.mark.parametrize( + ("typename", "key", "kwargs", "prefix", "expected_key"), + [ + ("UserNode", "name", {}, "p1", "p1:UserNode:name:{}"), + ( + "RepositoryNode", + "issues", + {"limit": 10}, + "p2", + """p2:RepositoryNode:issues:{"limit": 10}""", + ), + ( + "RepositoryNode", + "issues", + {"limit": 10, "state": "open"}, + "p3", + """p3:RepositoryNode:issues:{"limit": 10, "state": "open"}""", + ), + ( + "RepositoryNode", + "issues", + {"state": "open", "limit": 10}, + "p4", + """p4:RepositoryNode:issues:{"limit": 10, "state": "open"}""", + ), + ], +) +def test_generate_key(typename, key, kwargs, prefix, expected_key): + """Test cases for the generate_key method.""" + mock_info = MagicMock(spec=Info) + mock_info.path.typename = typename + mock_info.path.key = key + + extension = CacheFieldExtension(prefix=prefix) + assert extension.generate_key(mock_info, kwargs) == expected_key + + +class TestCacheFieldExtensionResolve: + """Test cases for the resolve method of CacheFieldExtension.""" + + @pytest.fixture + def mock_info(self): + """Return a mock Strawberry Info object.""" + mock_info = MagicMock(spec=Info) + mock_info.path.typename = "TestType" + mock_info.path.key = "testField" + return mock_info + + @patch("apps.common.extensions.cache") + def test_resolve_caches_result_on_miss(self, mock_cache, mock_info): + """Test that the resolver caches the result on a cache miss.""" + mock_cache.get.return_value = None + resolver_result = "some data" + next_ = MagicMock(return_value=resolver_result) + extension = CacheFieldExtension(cache_timeout=60) + + result = extension.resolve(next_, source=None, info=mock_info) + + assert result == resolver_result + mock_cache.get.assert_called_once() + next_.assert_called_once() + mock_cache.set.assert_called_once() + mock_cache.set.assert_called_with(mock_cache.get.call_args[0][0], resolver_result, 60) + + @patch("apps.common.extensions.cache") + def test_resolve_returns_cached_result_on_hit(self, mock_cache, mock_info): + """Test that the resolver returns the cached result on a cache hit.""" + cached_result = "cached data" + mock_cache.get.return_value = cached_result + next_ = MagicMock() + extension = CacheFieldExtension() + + result = extension.resolve(next_, source=None, info=mock_info) + + assert result == cached_result + mock_cache.get.assert_called_once() + next_.assert_not_called() + mock_cache.set.assert_not_called() + + @pytest.mark.parametrize("falsy_result", [None, [], {}, 0, False]) + @patch("apps.common.extensions.cache") + def test_resolve_does_not_cache_falsy_result(self, mock_cache, falsy_result, mock_info): + """Test that the resolver does not cache None or other falsy results.""" + mock_cache.get.return_value = None + next_ = MagicMock(return_value=falsy_result) + extension = CacheFieldExtension() + + result = extension.resolve(next_, source=None, info=mock_info) + + assert result == falsy_result + mock_cache.get.assert_called_once() + next_.assert_called_once() + mock_cache.set.assert_not_called() From 33af60c0db415e84f3b89a00505170fb69f0e7ca Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sun, 2 Nov 2025 18:16:29 +0530 Subject: [PATCH 3/7] cache falsy values; coderabbit suggestions --- backend/apps/common/extensions.py | 14 +++++----- backend/tests/apps/common/extensions_test.py | 29 +++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/backend/apps/common/extensions.py b/backend/apps/common/extensions.py index dd8245a57a..943b99c011 100644 --- a/backend/apps/common/extensions.py +++ b/backend/apps/common/extensions.py @@ -4,6 +4,7 @@ from django.conf import settings from django.core.cache import cache +from django.core.serializers.json import DjangoJSONEncoder from strawberry.extensions.field_extension import FieldExtension @@ -32,17 +33,16 @@ def generate_key(self, info, kwargs: dict) -> str: str: The unique cache key. """ - args_str = json.dumps(kwargs, sort_keys=True) + args_str = json.dumps(kwargs, sort_keys=True, cls=DjangoJSONEncoder) return f"{self.prefix}:{info.path.typename}:{info.path.key}:{args_str}" def resolve(self, next_, source, info, **kwargs): """Wrap the resolver to provide caching.""" cache_key = self.generate_key(info, kwargs) - if cached_result := cache.get(cache_key): - return cached_result - if result := next_(source, info, **kwargs): - cache.set(cache_key, result, self.cache_timeout) - - return result + return cache.get_or_set( + cache_key, + lambda: next_(source, info, **kwargs), + timeout=self.cache_timeout, + ) diff --git a/backend/tests/apps/common/extensions_test.py b/backend/tests/apps/common/extensions_test.py index eb0e6a950b..ac0bb3e5d7 100644 --- a/backend/tests/apps/common/extensions_test.py +++ b/backend/tests/apps/common/extensions_test.py @@ -56,46 +56,49 @@ def mock_info(self): @patch("apps.common.extensions.cache") def test_resolve_caches_result_on_miss(self, mock_cache, mock_info): - """Test that the resolver caches the result on a cache miss.""" - mock_cache.get.return_value = None + """Test that get_or_set calls the resolver on a cache miss.""" resolver_result = "some data" next_ = MagicMock(return_value=resolver_result) extension = CacheFieldExtension(cache_timeout=60) + def cache_miss_side_effect(key, default_callable, timeout=None): # noqa: ARG001 + return default_callable() + + mock_cache.get_or_set.side_effect = cache_miss_side_effect + result = extension.resolve(next_, source=None, info=mock_info) assert result == resolver_result - mock_cache.get.assert_called_once() + mock_cache.get_or_set.assert_called_once() next_.assert_called_once() - mock_cache.set.assert_called_once() - mock_cache.set.assert_called_with(mock_cache.get.call_args[0][0], resolver_result, 60) @patch("apps.common.extensions.cache") def test_resolve_returns_cached_result_on_hit(self, mock_cache, mock_info): """Test that the resolver returns the cached result on a cache hit.""" cached_result = "cached data" - mock_cache.get.return_value = cached_result + mock_cache.get_or_set.return_value = cached_result next_ = MagicMock() extension = CacheFieldExtension() result = extension.resolve(next_, source=None, info=mock_info) assert result == cached_result - mock_cache.get.assert_called_once() + mock_cache.get_or_set.assert_called_once() next_.assert_not_called() - mock_cache.set.assert_not_called() @pytest.mark.parametrize("falsy_result", [None, [], {}, 0, False]) @patch("apps.common.extensions.cache") - def test_resolve_does_not_cache_falsy_result(self, mock_cache, falsy_result, mock_info): - """Test that the resolver does not cache None or other falsy results.""" - mock_cache.get.return_value = None + def test_resolve_caches_falsy_result(self, mock_cache, falsy_result, mock_info): + """Test that the resolver caches None and other falsy results.""" next_ = MagicMock(return_value=falsy_result) extension = CacheFieldExtension() + def cache_miss_side_effect(key, default_callable, timeout=None): # noqa: ARG001 + return default_callable() + + mock_cache.get_or_set.side_effect = cache_miss_side_effect result = extension.resolve(next_, source=None, info=mock_info) assert result == falsy_result - mock_cache.get.assert_called_once() + mock_cache.get_or_set.assert_called_once() next_.assert_called_once() - mock_cache.set.assert_not_called() From c23b60c29c0e4942f6f87b6115689d51d4944624 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sun, 2 Nov 2025 22:07:06 +0530 Subject: [PATCH 4/7] fix: nested node caching by implementing path based key generation --- backend/apps/common/extensions.py | 26 ++++-- backend/tests/apps/common/extensions_test.py | 91 +++++++++++++++----- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/backend/apps/common/extensions.py b/backend/apps/common/extensions.py index 943b99c011..6ed1db82e1 100644 --- a/backend/apps/common/extensions.py +++ b/backend/apps/common/extensions.py @@ -1,11 +1,13 @@ """Strawberry extensions.""" import json +from typing import Any from django.conf import settings from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder from strawberry.extensions.field_extension import FieldExtension +from strawberry.types.info import Info class CacheFieldExtension(FieldExtension): @@ -22,10 +24,20 @@ def __init__(self, cache_timeout: int | None = None, prefix: str | None = None): self.cache_timeout = cache_timeout or settings.GRAPHQL_RESOLVER_CACHE_TIME_SECONDS self.prefix = prefix or settings.GRAPHQL_RESOLVER_CACHE_PREFIX - def generate_key(self, info, kwargs: dict) -> str: + def _convert_path_to_str(self, path: Any) -> str: + """Convert the Strawberry path linked list to a string.""" + parts = [] + current = path + while current: + parts.append(str(current.key)) + current = getattr(current, "prev", None) + return ".".join(reversed(parts)) + + def generate_key(self, source: Any | None, info: Info, kwargs: dict) -> str: """Generate a unique cache key for a field. Args: + source (Any | None): The source/parent object. info (Info): The Strawberry execution info. kwargs (dict): The resolver's arguments. @@ -33,13 +45,17 @@ def generate_key(self, info, kwargs: dict) -> str: str: The unique cache key. """ - args_str = json.dumps(kwargs, sort_keys=True, cls=DjangoJSONEncoder) + key_kwargs = kwargs.copy() + if source and (source_id := getattr(source, "id", None)) is not None: + key_kwargs["__source_id__"] = str(source_id) + + args_str = json.dumps(key_kwargs, sort_keys=True, cls=DjangoJSONEncoder) - return f"{self.prefix}:{info.path.typename}:{info.path.key}:{args_str}" + return f"{self.prefix}:{self._convert_path_to_str(info.path)}:{args_str}" - def resolve(self, next_, source, info, **kwargs): + def resolve(self, next_: Any, source: Any, info: Info, **kwargs: Any) -> Any: """Wrap the resolver to provide caching.""" - cache_key = self.generate_key(info, kwargs) + cache_key = self.generate_key(source, info, kwargs) return cache.get_or_set( cache_key, diff --git a/backend/tests/apps/common/extensions_test.py b/backend/tests/apps/common/extensions_test.py index ac0bb3e5d7..80c06f8be4 100644 --- a/backend/tests/apps/common/extensions_test.py +++ b/backend/tests/apps/common/extensions_test.py @@ -6,41 +6,93 @@ from apps.common.extensions import CacheFieldExtension +class MockPath: + def __init__(self, key, typename, prev=None): + self.key = key + self.prev = prev + self.typename = typename + + +class MockSource: + def __init__(self, id=None): # noqa: A002 + self.id = id + + @pytest.mark.parametrize( - ("typename", "key", "kwargs", "prefix", "expected_key"), + ("source", "path", "kwargs", "prefix", "expected_key"), [ - ("UserNode", "name", {}, "p1", "p1:UserNode:name:{}"), ( - "RepositoryNode", - "issues", - {"limit": 10}, + None, + MockPath(key="repository", typename="Query"), + {"organization_key": "OWASP", "repository_key": "nest"}, + "p1", + 'p1:repository:{"organization_key": "OWASP", "repository_key": "nest"}', + ), + ( + MockSource(id=123), + MockPath( + key="organization", + typename="RepositoryNode", + prev=MockPath(key="repository", typename="Query"), + ), + {}, "p2", - """p2:RepositoryNode:issues:{"limit": 10}""", + 'p2:repository.organization:{"__source_id__": "123"}', ), ( - "RepositoryNode", - "issues", - {"limit": 10, "state": "open"}, + MockSource(id=0), + MockPath( + key="organization", + typename="RepositoryNode", + prev=MockPath(key="repository", typename="Query"), + ), + {}, "p3", - """p3:RepositoryNode:issues:{"limit": 10, "state": "open"}""", + 'p3:repository.organization:{"__source_id__": "0"}', ), ( - "RepositoryNode", - "issues", - {"state": "open", "limit": 10}, + MockSource(), + MockPath( + key="organization", + typename="RepositoryNode", + prev=MockPath(key="repository", typename="Query"), + ), + {}, "p4", - """p4:RepositoryNode:issues:{"limit": 10, "state": "open"}""", + "p4:repository.organization:{}", + ), + ( + None, + MockPath( + key="badgeCount", + typename="UserNode", + prev=MockPath( + key="author", + typename="IssueNode", + prev=MockPath( + key=0, + typename=None, + prev=MockPath( + key="issues", + typename="RepositoryNode", + prev=MockPath(key="repository", typename="Query"), + ), + ), + ), + ), + {}, + "graphql-resolver", + "graphql-resolver:repository.issues.0.author.badgeCount:{}", ), ], ) -def test_generate_key(typename, key, kwargs, prefix, expected_key): +def test_generate_key(source, path, kwargs, prefix, expected_key): """Test cases for the generate_key method.""" mock_info = MagicMock(spec=Info) - mock_info.path.typename = typename - mock_info.path.key = key + mock_info.path = path extension = CacheFieldExtension(prefix=prefix) - assert extension.generate_key(mock_info, kwargs) == expected_key + assert extension.generate_key(source, mock_info, kwargs) == expected_key class TestCacheFieldExtensionResolve: @@ -50,8 +102,7 @@ class TestCacheFieldExtensionResolve: def mock_info(self): """Return a mock Strawberry Info object.""" mock_info = MagicMock(spec=Info) - mock_info.path.typename = "TestType" - mock_info.path.key = "testField" + mock_info.path = MockPath(key="testField", typename="TestType", prev=None) return mock_info @patch("apps.common.extensions.cache") From c1cd5b216212ac2fbb9b963177a1d02e7052f10e Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sun, 2 Nov 2025 22:48:01 +0530 Subject: [PATCH 5/7] use CacheFieldExtension in high-cost fields --- backend/apps/github/api/internal/nodes/issue.py | 3 ++- .../apps/github/api/internal/nodes/milestone.py | 3 ++- .../github/api/internal/nodes/organization.py | 3 ++- .../github/api/internal/nodes/pull_request.py | 3 ++- backend/apps/github/api/internal/nodes/release.py | 3 ++- .../apps/github/api/internal/nodes/repository.py | 13 +++++++------ backend/apps/github/api/internal/nodes/user.py | 4 ++-- backend/apps/github/api/internal/queries/issue.py | 3 ++- .../apps/github/api/internal/queries/milestone.py | 3 ++- .../github/api/internal/queries/organization.py | 3 ++- .../github/api/internal/queries/pull_request.py | 3 ++- .../apps/github/api/internal/queries/release.py | 3 ++- .../github/api/internal/queries/repository.py | 5 +++-- .../internal/queries/repository_contributor.py | 3 ++- backend/apps/github/api/internal/queries/user.py | 5 +++-- .../apps/mentorship/api/internal/nodes/module.py | 3 ++- .../apps/mentorship/api/internal/nodes/program.py | 3 ++- .../mentorship/api/internal/queries/mentorship.py | 3 ++- .../mentorship/api/internal/queries/module.py | 7 ++++--- .../mentorship/api/internal/queries/program.py | 3 ++- .../api/internal/nodes/board_of_directors.py | 5 +++-- backend/apps/owasp/api/internal/nodes/chapter.py | 3 ++- backend/apps/owasp/api/internal/nodes/common.py | 5 +++-- .../owasp/api/internal/nodes/member_snapshot.py | 3 ++- backend/apps/owasp/api/internal/nodes/project.py | 15 ++++++++------- backend/apps/owasp/api/internal/nodes/snapshot.py | 11 ++++++----- .../api/internal/queries/board_of_directors.py | 5 +++-- .../apps/owasp/api/internal/queries/chapter.py | 5 +++-- .../apps/owasp/api/internal/queries/committee.py | 3 ++- backend/apps/owasp/api/internal/queries/event.py | 3 ++- .../owasp/api/internal/queries/member_snapshot.py | 5 +++-- backend/apps/owasp/api/internal/queries/post.py | 3 ++- .../apps/owasp/api/internal/queries/project.py | 9 +++++---- .../internal/queries/project_health_metrics.py | 4 ++++ .../apps/owasp/api/internal/queries/snapshot.py | 5 +++-- .../apps/owasp/api/internal/queries/sponsor.py | 3 ++- backend/apps/owasp/api/internal/queries/stats.py | 3 ++- 37 files changed, 104 insertions(+), 65 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 4ac7eb7672..03d4ebde02 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.user import UserNode from apps.github.models.issue import Issue @@ -19,7 +20,7 @@ class IssueNode(strawberry.relay.Node): """GitHub issue node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def author(self) -> UserNode | None: """Resolve author.""" return self.author diff --git a/backend/apps/github/api/internal/nodes/milestone.py b/backend/apps/github/api/internal/nodes/milestone.py index 17b5d67f2c..8ce18ec614 100644 --- a/backend/apps/github/api/internal/nodes/milestone.py +++ b/backend/apps/github/api/internal/nodes/milestone.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.user import UserNode from apps.github.models.milestone import Milestone @@ -22,7 +23,7 @@ class MilestoneNode(strawberry.relay.Node): """Github Milestone Node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def author(self) -> UserNode | None: """Resolve author.""" return self.author diff --git a/backend/apps/github/api/internal/nodes/organization.py b/backend/apps/github/api/internal/nodes/organization.py index a249846bc7..5a4f29ac32 100644 --- a/backend/apps/github/api/internal/nodes/organization.py +++ b/backend/apps/github/api/internal/nodes/organization.py @@ -4,6 +4,7 @@ import strawberry_django from django.db import models +from apps.common.extensions import CacheFieldExtension from apps.github.models.organization import Organization from apps.github.models.repository import Repository from apps.github.models.repository_contributor import RepositoryContributor @@ -39,7 +40,7 @@ class OrganizationStatsNode: class OrganizationNode(strawberry.relay.Node): """GitHub organization node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def stats(self) -> OrganizationStatsNode: """Resolve organization stats.""" repositories = Repository.objects.filter(organization=self) diff --git a/backend/apps/github/api/internal/nodes/pull_request.py b/backend/apps/github/api/internal/nodes/pull_request.py index 8ac55e2221..8aceaacbda 100644 --- a/backend/apps/github/api/internal/nodes/pull_request.py +++ b/backend/apps/github/api/internal/nodes/pull_request.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.user import UserNode from apps.github.models.pull_request import PullRequest @@ -17,7 +18,7 @@ class PullRequestNode(strawberry.relay.Node): """GitHub pull request node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def author(self) -> UserNode | None: """Resolve author.""" return self.author diff --git a/backend/apps/github/api/internal/nodes/release.py b/backend/apps/github/api/internal/nodes/release.py index 0bf8e78cdf..3681e0f61f 100644 --- a/backend/apps/github/api/internal/nodes/release.py +++ b/backend/apps/github/api/internal/nodes/release.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.user import UserNode from apps.github.models.release import Release from apps.owasp.constants import OWASP_ORGANIZATION_NAME @@ -20,7 +21,7 @@ class ReleaseNode(strawberry.relay.Node): """GitHub release node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def author(self) -> UserNode | None: """Resolve author.""" return self.author diff --git a/backend/apps/github/api/internal/nodes/repository.py b/backend/apps/github/api/internal/nodes/repository.py index e0791fe4c4..3108e92184 100644 --- a/backend/apps/github/api/internal/nodes/repository.py +++ b/backend/apps/github/api/internal/nodes/repository.py @@ -5,6 +5,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.milestone import MilestoneNode from apps.github.api.internal.nodes.organization import OrganizationNode @@ -41,7 +42,7 @@ class RepositoryNode(strawberry.relay.Node): """Repository node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def issues(self) -> list[IssueNode]: """Resolve recent issues.""" # TODO(arkid15r): rename this to recent_issues. @@ -57,7 +58,7 @@ def latest_release(self) -> str: """Resolve latest release.""" return self.latest_release - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def organization(self) -> OrganizationNode | None: """Resolve organization.""" return self.organization @@ -67,25 +68,25 @@ def owner_key(self) -> str: """Resolve owner key.""" return self.owner_key - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def project( self, ) -> Annotated["ProjectNode", strawberry.lazy("apps.owasp.api.internal.nodes.project")] | None: """Resolve project.""" return self.project - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" return self.recent_milestones.select_related("repository").order_by("-created_at")[:limit] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def releases(self) -> list[ReleaseNode]: """Resolve recent releases.""" # TODO(arkid15r): rename this to recent_releases. return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def top_contributors(self) -> list[RepositoryContributorNode]: """Resolve top contributors.""" return self.idx_top_contributors diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index c305d7764e..8f88e04972 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.models.user import User from apps.nest.api.internal.nodes.badge import BadgeNode @@ -28,12 +29,11 @@ class UserNode: """GitHub user node.""" - @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" return self.user_badges.filter(is_active=True).count() - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def badges(self) -> list[BadgeNode]: """Return user badges.""" user_badges = ( diff --git a/backend/apps/github/api/internal/queries/issue.py b/backend/apps/github/api/internal/queries/issue.py index 87cb288401..f422778dd0 100644 --- a/backend/apps/github/api/internal/queries/issue.py +++ b/backend/apps/github/api/internal/queries/issue.py @@ -3,6 +3,7 @@ import strawberry from django.db.models import OuterRef, Subquery +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.issue import IssueNode from apps.github.models.issue import Issue @@ -11,7 +12,7 @@ class IssueQuery: """GraphQL query class for retrieving GitHub issues.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_issues( self, *, diff --git a/backend/apps/github/api/internal/queries/milestone.py b/backend/apps/github/api/internal/queries/milestone.py index 8c95ff55cf..07a4af9f6c 100644 --- a/backend/apps/github/api/internal/queries/milestone.py +++ b/backend/apps/github/api/internal/queries/milestone.py @@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError from django.db.models import OuterRef, Subquery +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.milestone import MilestoneNode from apps.github.models.milestone import Milestone @@ -12,7 +13,7 @@ class MilestoneQuery: """Github Milestone Queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_milestones( self, *, diff --git a/backend/apps/github/api/internal/queries/organization.py b/backend/apps/github/api/internal/queries/organization.py index 996b287b62..e88b7a3ce0 100644 --- a/backend/apps/github/api/internal/queries/organization.py +++ b/backend/apps/github/api/internal/queries/organization.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.organization import OrganizationNode from apps.github.models.organization import Organization @@ -10,7 +11,7 @@ class OrganizationQuery: """Organization queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def organization( self, *, diff --git a/backend/apps/github/api/internal/queries/pull_request.py b/backend/apps/github/api/internal/queries/pull_request.py index 4649a55a91..e0cb24c0bc 100644 --- a/backend/apps/github/api/internal/queries/pull_request.py +++ b/backend/apps/github/api/internal/queries/pull_request.py @@ -3,6 +3,7 @@ import strawberry from django.db.models import OuterRef, Subquery +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.models.pull_request import PullRequest from apps.owasp.models.project import Project @@ -12,7 +13,7 @@ class PullRequestQuery: """Pull request queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_pull_requests( self, *, diff --git a/backend/apps/github/api/internal/queries/release.py b/backend/apps/github/api/internal/queries/release.py index f9e3a60b6c..4730933c83 100644 --- a/backend/apps/github/api/internal/queries/release.py +++ b/backend/apps/github/api/internal/queries/release.py @@ -3,6 +3,7 @@ import strawberry from django.db.models import OuterRef, Subquery +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.models.release import Release @@ -11,7 +12,7 @@ class ReleaseQuery: """GraphQL query class for retrieving recent GitHub releases.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_releases( self, *, diff --git a/backend/apps/github/api/internal/queries/repository.py b/backend/apps/github/api/internal/queries/repository.py index b7bf1a59c7..219c6961a6 100644 --- a/backend/apps/github/api/internal/queries/repository.py +++ b/backend/apps/github/api/internal/queries/repository.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.repository import RepositoryNode from apps.github.models.repository import Repository @@ -10,7 +11,7 @@ class RepositoryQuery: """Repository queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def repository( self, organization_key: str, @@ -34,7 +35,7 @@ def repository( except Repository.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def repositories( self, organization: str, diff --git a/backend/apps/github/api/internal/queries/repository_contributor.py b/backend/apps/github/api/internal/queries/repository_contributor.py index fd2716c8ba..1d00204a41 100644 --- a/backend/apps/github/api/internal/queries/repository_contributor.py +++ b/backend/apps/github/api/internal/queries/repository_contributor.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode from apps.github.models.repository_contributor import RepositoryContributor @@ -10,7 +11,7 @@ class RepositoryContributorQuery: """Repository contributor queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def top_contributors( self, *, diff --git a/backend/apps/github/api/internal/queries/user.py b/backend/apps/github/api/internal/queries/user.py index 199c33aa90..dc132845b9 100644 --- a/backend/apps/github/api/internal/queries/user.py +++ b/backend/apps/github/api/internal/queries/user.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.repository import RepositoryNode from apps.github.api.internal.nodes.user import UserNode from apps.github.models.repository_contributor import RepositoryContributor @@ -12,7 +13,7 @@ class UserQuery: """User queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def top_contributed_repositories( self, login: str, @@ -36,7 +37,7 @@ def top_contributed_repositories( .order_by("-contributions_count") ] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def user( self, login: str, diff --git a/backend/apps/mentorship/api/internal/nodes/module.py b/backend/apps/mentorship/api/internal/nodes/module.py index fa94ad4728..77f61881b1 100644 --- a/backend/apps/mentorship/api/internal/nodes/module.py +++ b/backend/apps/mentorship/api/internal/nodes/module.py @@ -4,6 +4,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum from apps.mentorship.api.internal.nodes.mentor import MentorNode from apps.mentorship.api.internal.nodes.program import ProgramNode @@ -25,7 +26,7 @@ class ModuleNode: started_at: datetime tags: list[str] | None = None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def mentors(self) -> list[MentorNode]: """Get the list of mentors for this module.""" return self.mentors.all() diff --git a/backend/apps/mentorship/api/internal/nodes/program.py b/backend/apps/mentorship/api/internal/nodes/program.py index 463caa076f..9da31f5b80 100644 --- a/backend/apps/mentorship/api/internal/nodes/program.py +++ b/backend/apps/mentorship/api/internal/nodes/program.py @@ -4,6 +4,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.mentorship.api.internal.nodes.enum import ( ExperienceLevelEnum, ProgramStatusEnum, @@ -28,7 +29,7 @@ class ProgramNode: user_role: str | None = None tags: list[str] | None = None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def admins(self) -> list[MentorNode] | None: """Get the list of program administrators.""" return self.admins.all() diff --git a/backend/apps/mentorship/api/internal/queries/mentorship.py b/backend/apps/mentorship/api/internal/queries/mentorship.py index 90bd1f7a8b..eb50bd8317 100644 --- a/backend/apps/mentorship/api/internal/queries/mentorship.py +++ b/backend/apps/mentorship/api/internal/queries/mentorship.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.models.user import User as GithubUser from apps.mentorship.models.mentor import Mentor @@ -17,7 +18,7 @@ class UserRolesResult: class MentorshipQuery: """GraphQL queries for mentorship-related data.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def is_mentor(self, login: str) -> bool: """Check if a GitHub login is a mentor.""" if not login or not login.strip(): diff --git a/backend/apps/mentorship/api/internal/queries/module.py b/backend/apps/mentorship/api/internal/queries/module.py index 8fec753752..16fd65c265 100644 --- a/backend/apps/mentorship/api/internal/queries/module.py +++ b/backend/apps/mentorship/api/internal/queries/module.py @@ -5,6 +5,7 @@ import strawberry from django.core.exceptions import ObjectDoesNotExist +from apps.common.extensions import CacheFieldExtension from apps.mentorship.api.internal.nodes.module import ModuleNode from apps.mentorship.models import Module @@ -15,7 +16,7 @@ class ModuleQuery: """Module queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def get_program_modules(self, program_key: str) -> list[ModuleNode]: """Get all modules by program Key. Returns an empty list if program is not found.""" return ( @@ -25,7 +26,7 @@ def get_program_modules(self, program_key: str) -> list[ModuleNode]: .order_by("started_at") ) - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def get_project_modules(self, project_key: str) -> list[ModuleNode]: """Get all modules by project Key. Returns an empty list if project is not found.""" return ( @@ -35,7 +36,7 @@ def get_project_modules(self, project_key: str) -> list[ModuleNode]: .order_by("started_at") ) - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def get_module(self, module_key: str, program_key: str) -> ModuleNode: """Get a single module by its key within a specific program.""" try: diff --git a/backend/apps/mentorship/api/internal/queries/program.py b/backend/apps/mentorship/api/internal/queries/program.py index 0abf8a263d..163ef79d9e 100644 --- a/backend/apps/mentorship/api/internal/queries/program.py +++ b/backend/apps/mentorship/api/internal/queries/program.py @@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q +from apps.common.extensions import CacheFieldExtension from apps.mentorship.api.internal.nodes.program import PaginatedPrograms, ProgramNode from apps.mentorship.models import Program from apps.mentorship.models.mentor import Mentor @@ -19,7 +20,7 @@ class ProgramQuery: """Program queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def get_program(self, program_key: str) -> ProgramNode: """Get a program by Key.""" try: diff --git a/backend/apps/owasp/api/internal/nodes/board_of_directors.py b/backend/apps/owasp/api/internal/nodes/board_of_directors.py index 71e9c35577..99e602f36d 100644 --- a/backend/apps/owasp/api/internal/nodes/board_of_directors.py +++ b/backend/apps/owasp/api/internal/nodes/board_of_directors.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.entity_member import EntityMemberNode from apps.owasp.models.board_of_directors import BoardOfDirectors @@ -18,12 +19,12 @@ class BoardOfDirectorsNode(strawberry.relay.Node): """Board of Directors node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def candidates(self) -> list[EntityMemberNode]: """Resolve board election candidates.""" return self.get_candidates() # type: ignore[call-arg] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def members(self) -> list[EntityMemberNode]: """Resolve board members.""" return self.get_members() # type: ignore[call-arg] diff --git a/backend/apps/owasp/api/internal/nodes/chapter.py b/backend/apps/owasp/api/internal/nodes/chapter.py index 23eb7a7fb6..cebb215eab 100644 --- a/backend/apps/owasp/api/internal/nodes/chapter.py +++ b/backend/apps/owasp/api/internal/nodes/chapter.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.common import GenericEntityNode from apps.owasp.models.chapter import Chapter @@ -36,7 +37,7 @@ def created_at(self) -> float: """Resolve created at.""" return self.idx_created_at - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def geo_location(self) -> GeoLocationType | None: """Resolve geographic location.""" return ( diff --git a/backend/apps/owasp/api/internal/nodes/common.py b/backend/apps/owasp/api/internal/nodes/common.py index a692265fad..2639d65a9d 100644 --- a/backend/apps/owasp/api/internal/nodes/common.py +++ b/backend/apps/owasp/api/internal/nodes/common.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode from apps.owasp.api.internal.nodes.entity_member import EntityMemberNode @@ -10,7 +11,7 @@ class GenericEntityNode(strawberry.relay.Node): """Base node class for OWASP entities with common fields and resolvers.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def entity_leaders(self) -> list[EntityMemberNode]: """Resolve entity leaders.""" return self.entity_leaders @@ -25,7 +26,7 @@ def related_urls(self) -> list[str]: """Resolve related URLs.""" return self.idx_related_urls - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def top_contributors(self) -> list[RepositoryContributorNode]: """Resolve top contributors.""" return [RepositoryContributorNode(**tc) for tc in self.idx_top_contributors] diff --git a/backend/apps/owasp/api/internal/nodes/member_snapshot.py b/backend/apps/owasp/api/internal/nodes/member_snapshot.py index bb6a6701a2..b031dfde62 100644 --- a/backend/apps/owasp/api/internal/nodes/member_snapshot.py +++ b/backend/apps/owasp/api/internal/nodes/member_snapshot.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.user import UserNode from apps.owasp.models.member_snapshot import MemberSnapshot @@ -28,7 +29,7 @@ def commits_count(self) -> int: """Resolve commits count.""" return self.commits_count - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def github_user(self) -> UserNode: """Resolve GitHub user.""" return self.github_user diff --git a/backend/apps/owasp/api/internal/nodes/project.py b/backend/apps/owasp/api/internal/nodes/project.py index 1b881d44ef..15dc495000 100644 --- a/backend/apps/owasp/api/internal/nodes/project.py +++ b/backend/apps/owasp/api/internal/nodes/project.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.milestone import MilestoneNode from apps.github.api.internal.nodes.pull_request import PullRequestNode @@ -39,7 +40,7 @@ class ProjectNode(GenericEntityNode): """Project node.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def health_metrics_list(self, limit: int = 30) -> list[ProjectHealthMetricsNode]: """Resolve project health metrics.""" return ProjectHealthMetrics.objects.filter( @@ -48,7 +49,7 @@ def health_metrics_list(self, limit: int = 30) -> list[ProjectHealthMetricsNode] "nest_created_at", )[:limit] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def health_metrics_latest(self) -> ProjectHealthMetricsNode | None: """Resolve latest project health metrics.""" return ( @@ -74,29 +75,29 @@ def languages(self) -> list[str]: """Resolve languages.""" return self.idx_languages - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_issues(self) -> list[IssueNode]: """Resolve recent issues.""" return self.issues.select_related("author").order_by("-created_at")[:RECENT_ISSUES_LIMIT] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" return self.recent_milestones.select_related("author").order_by("-created_at")[:limit] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_pull_requests(self) -> list[PullRequestNode]: """Resolve recent pull requests.""" return self.pull_requests.select_related("author").order_by("-created_at")[ :RECENT_PULL_REQUESTS_LIMIT ] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_releases(self) -> list[ReleaseNode]: """Resolve recent releases.""" return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def repositories(self) -> list[RepositoryNode]: """Resolve repositories.""" return self.repositories.order_by("-pushed_at", "-updated_at") diff --git a/backend/apps/owasp/api/internal/nodes/snapshot.py b/backend/apps/owasp/api/internal/nodes/snapshot.py index 0e67f09509..090a2e616d 100644 --- a/backend/apps/owasp/api/internal/nodes/snapshot.py +++ b/backend/apps/owasp/api/internal/nodes/snapshot.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.github.api.internal.nodes.issue import IssueNode from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.api.internal.nodes.user import UserNode @@ -31,27 +32,27 @@ def key(self) -> str: """Resolve key.""" return self.key - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def new_chapters(self) -> list[ChapterNode]: """Resolve new chapters.""" return self.new_chapters.all() - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def new_issues(self) -> list[IssueNode]: """Resolve new issues.""" return self.new_issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def new_projects(self) -> list[ProjectNode]: """Resolve new projects.""" return self.new_projects.order_by("-created_at") - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def new_releases(self) -> list[ReleaseNode]: """Resolve new releases.""" return self.new_releases.order_by("-published_at") - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def new_users(self) -> list[UserNode]: """Resolve new users.""" return self.new_users.order_by("-created_at") diff --git a/backend/apps/owasp/api/internal/queries/board_of_directors.py b/backend/apps/owasp/api/internal/queries/board_of_directors.py index 9445386a03..f1c41f5c91 100644 --- a/backend/apps/owasp/api/internal/queries/board_of_directors.py +++ b/backend/apps/owasp/api/internal/queries/board_of_directors.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.board_of_directors import BoardOfDirectorsNode from apps.owasp.models.board_of_directors import BoardOfDirectors @@ -10,7 +11,7 @@ class BoardOfDirectorsQuery: """GraphQL queries for Board of Directors model.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def board_of_directors(self, year: int) -> BoardOfDirectorsNode | None: """Resolve Board of Directors by year. @@ -26,7 +27,7 @@ def board_of_directors(self, year: int) -> BoardOfDirectorsNode | None: except BoardOfDirectors.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def boards_of_directors(self, limit: int = 10) -> list[BoardOfDirectorsNode]: """Resolve multiple Board of Directors instances. diff --git a/backend/apps/owasp/api/internal/queries/chapter.py b/backend/apps/owasp/api/internal/queries/chapter.py index 8abebb5b5f..26338e2f05 100644 --- a/backend/apps/owasp/api/internal/queries/chapter.py +++ b/backend/apps/owasp/api/internal/queries/chapter.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.chapter import ChapterNode from apps.owasp.models.chapter import Chapter @@ -10,7 +11,7 @@ class ChapterQuery: """Chapter queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def chapter(self, key: str) -> ChapterNode | None: """Resolve chapter.""" try: @@ -18,7 +19,7 @@ def chapter(self, key: str) -> ChapterNode | None: except Chapter.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_chapters(self, limit: int = 8) -> list[ChapterNode]: """Resolve recent chapters.""" return Chapter.active_chapters.order_by("-created_at")[:limit] diff --git a/backend/apps/owasp/api/internal/queries/committee.py b/backend/apps/owasp/api/internal/queries/committee.py index 4cbdb7f3bd..57ab9ed92d 100644 --- a/backend/apps/owasp/api/internal/queries/committee.py +++ b/backend/apps/owasp/api/internal/queries/committee.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.committee import CommitteeNode from apps.owasp.models.committee import Committee @@ -10,7 +11,7 @@ class CommitteeQuery: """Committee queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def committee(self, key: str) -> CommitteeNode | None: """Resolve committee by key. diff --git a/backend/apps/owasp/api/internal/queries/event.py b/backend/apps/owasp/api/internal/queries/event.py index b08973201a..13dcb15d77 100644 --- a/backend/apps/owasp/api/internal/queries/event.py +++ b/backend/apps/owasp/api/internal/queries/event.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.event import EventNode from apps.owasp.models.event import Event @@ -10,7 +11,7 @@ class EventQuery: """Event queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def upcoming_events(self, limit: int = 6) -> list[EventNode]: """Resolve upcoming events.""" return Event.upcoming_events()[:limit] diff --git a/backend/apps/owasp/api/internal/queries/member_snapshot.py b/backend/apps/owasp/api/internal/queries/member_snapshot.py index f48b03f01b..86ebcb96c1 100644 --- a/backend/apps/owasp/api/internal/queries/member_snapshot.py +++ b/backend/apps/owasp/api/internal/queries/member_snapshot.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.github.models.user import User from apps.owasp.api.internal.nodes.member_snapshot import MemberSnapshotNode from apps.owasp.models.member_snapshot import MemberSnapshot @@ -11,7 +12,7 @@ class MemberSnapshotQuery: """GraphQL queries for MemberSnapshot model.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def member_snapshot( self, user_login: str, start_year: int | None = None ) -> MemberSnapshotNode | None: @@ -38,7 +39,7 @@ def member_snapshot( except User.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def member_snapshots( self, user_login: str | None = None, limit: int = 10 ) -> list[MemberSnapshotNode]: diff --git a/backend/apps/owasp/api/internal/queries/post.py b/backend/apps/owasp/api/internal/queries/post.py index 28c1d88817..bc2879b924 100644 --- a/backend/apps/owasp/api/internal/queries/post.py +++ b/backend/apps/owasp/api/internal/queries/post.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.post import PostNode from apps.owasp.models.post import Post @@ -10,7 +11,7 @@ class PostQuery: """GraphQL queries for Post model.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_posts(self, limit: int = 5) -> list[PostNode]: """Return the 5 most recent posts.""" return Post.recent_posts()[:limit] diff --git a/backend/apps/owasp/api/internal/queries/project.py b/backend/apps/owasp/api/internal/queries/project.py index 154e25bfe9..9b89ff1802 100644 --- a/backend/apps/owasp/api/internal/queries/project.py +++ b/backend/apps/owasp/api/internal/queries/project.py @@ -3,6 +3,7 @@ import strawberry from django.db.models import Q +from apps.common.extensions import CacheFieldExtension from apps.github.models.user import User as GithubUser from apps.owasp.api.internal.nodes.project import ProjectNode from apps.owasp.models.project import Project @@ -12,7 +13,7 @@ class ProjectQuery: """Project queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def project(self, key: str) -> ProjectNode | None: """Resolve project. @@ -28,7 +29,7 @@ def project(self, key: str) -> ProjectNode | None: except Project.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def recent_projects(self, limit: int = 8) -> list[ProjectNode]: """Resolve recent projects. @@ -41,7 +42,7 @@ def recent_projects(self, limit: int = 8) -> list[ProjectNode]: """ return Project.objects.filter(is_active=True).order_by("-created_at")[:limit] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def search_projects(self, query: str) -> list[ProjectNode]: """Search active projects by name (case-insensitive, partial match).""" if not query.strip(): @@ -52,7 +53,7 @@ def search_projects(self, query: str) -> list[ProjectNode]: name__icontains=query.strip(), ).order_by("name")[:3] - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def is_project_leader(self, info: strawberry.Info, login: str) -> bool: """Check if a GitHub login or name is listed as a project leader.""" try: diff --git a/backend/apps/owasp/api/internal/queries/project_health_metrics.py b/backend/apps/owasp/api/internal/queries/project_health_metrics.py index ead03bf330..090abc0ef0 100644 --- a/backend/apps/owasp/api/internal/queries/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/queries/project_health_metrics.py @@ -3,6 +3,7 @@ import strawberry import strawberry_django +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.filters.project_health_metrics import ProjectHealthMetricsFilter from apps.owasp.api.internal.nodes.project_health_metrics import ProjectHealthMetricsNode from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode @@ -21,6 +22,7 @@ class ProjectHealthMetricsQuery: pagination=True, ordering=ProjectHealthMetricsOrder, permission_classes=[HasDashboardAccess], + extensions=[CacheFieldExtension()], ) def project_health_metrics( self, @@ -43,6 +45,7 @@ def project_health_metrics( @strawberry.field( permission_classes=[HasDashboardAccess], + extensions=[CacheFieldExtension()], ) def project_health_stats(self) -> ProjectHealthStatsNode: """Resolve overall project health stats. @@ -55,6 +58,7 @@ def project_health_stats(self) -> ProjectHealthStatsNode: @strawberry.field( permission_classes=[HasDashboardAccess], + extensions=[CacheFieldExtension()], ) def project_health_metrics_distinct_length( self, diff --git a/backend/apps/owasp/api/internal/queries/snapshot.py b/backend/apps/owasp/api/internal/queries/snapshot.py index a70c8d264d..5a59112bf9 100644 --- a/backend/apps/owasp/api/internal/queries/snapshot.py +++ b/backend/apps/owasp/api/internal/queries/snapshot.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.snapshot import SnapshotNode from apps.owasp.models.snapshot import Snapshot @@ -10,7 +11,7 @@ class SnapshotQuery: """Snapshot queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def snapshot(self, key: str) -> SnapshotNode | None: """Resolve snapshot by key.""" try: @@ -21,7 +22,7 @@ def snapshot(self, key: str) -> SnapshotNode | None: except Snapshot.DoesNotExist: return None - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def snapshots(self, limit: int = 12) -> list[SnapshotNode]: """Resolve snapshots.""" return Snapshot.objects.filter( diff --git a/backend/apps/owasp/api/internal/queries/sponsor.py b/backend/apps/owasp/api/internal/queries/sponsor.py index 3e692a7ec8..0faf791463 100644 --- a/backend/apps/owasp/api/internal/queries/sponsor.py +++ b/backend/apps/owasp/api/internal/queries/sponsor.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.owasp.api.internal.nodes.sponsor import SponsorNode from apps.owasp.models.sponsor import Sponsor @@ -10,7 +11,7 @@ class SponsorQuery: """Sponsor queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def sponsors(self) -> list[SponsorNode]: """Resolve sponsors.""" return sorted( diff --git a/backend/apps/owasp/api/internal/queries/stats.py b/backend/apps/owasp/api/internal/queries/stats.py index 21cdc9a617..e8a9195deb 100644 --- a/backend/apps/owasp/api/internal/queries/stats.py +++ b/backend/apps/owasp/api/internal/queries/stats.py @@ -2,6 +2,7 @@ import strawberry +from apps.common.extensions import CacheFieldExtension from apps.common.utils import round_down from apps.github.models.user import User from apps.owasp.api.internal.nodes.stats import StatsNode @@ -14,7 +15,7 @@ class StatsQuery: """Stats queries.""" - @strawberry.field + @strawberry.field(extensions=[CacheFieldExtension()]) def stats_overview(self) -> StatsNode: """Resolve stats overview.""" active_projects_stats = Project.active_projects_count() From f9a2f4ebead7bb751b09f9055fa1326795e3bbbf Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sun, 2 Nov 2025 22:59:36 +0530 Subject: [PATCH 6/7] fix missing badge_count --- backend/apps/github/api/internal/nodes/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 8f88e04972..4f6cc16c86 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -29,6 +29,7 @@ class UserNode: """GitHub user node.""" + @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" return self.user_badges.filter(is_active=True).count() From cb4820e58adea7f65beab12fd6a01804eb4570ce Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Sun, 2 Nov 2025 23:49:41 +0530 Subject: [PATCH 7/7] Update code --- backend/apps/common/extensions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/apps/common/extensions.py b/backend/apps/common/extensions.py index 6ed1db82e1..95bafb4ce2 100644 --- a/backend/apps/common/extensions.py +++ b/backend/apps/common/extensions.py @@ -55,10 +55,8 @@ def generate_key(self, source: Any | None, info: Info, kwargs: dict) -> str: def resolve(self, next_: Any, source: Any, info: Info, **kwargs: Any) -> Any: """Wrap the resolver to provide caching.""" - cache_key = self.generate_key(source, info, kwargs) - return cache.get_or_set( - cache_key, + self.generate_key(source, info, kwargs), lambda: next_(source, info, **kwargs), timeout=self.cache_timeout, )