feat: add permission-aware DocType discovery

This commit is contained in:
Julien Denizot
2026-06-20 12:35:26 +02:00
parent 73d27dacaa
commit f5f9e27371
5 changed files with 173 additions and 2 deletions
@@ -18,7 +18,7 @@ def capabilities() -> dict[str, Any]:
"installed_apps": sorted(frappe.get_installed_apps()),
"capabilities": {
"system_information": True,
"doctype_discovery": False,
"doctype_discovery": True,
"document_retrieval": False,
"rag_indexing": False,
"write_operations": False,
+99
View File
@@ -0,0 +1,99 @@
from __future__ import annotations
from typing import Any
import frappe
from frappe import _
from frappe.permissions import get_doctypes_with_read
from frappe.utils import cint
DEFAULT_PAGE_LENGTH = 50
MAX_PAGE_LENGTH = 100
SAFE_DOCTYPE_FIELDS = (
"name",
"module",
"custom",
"issingle",
"is_submittable",
)
@frappe.whitelist(methods=["GET"])
def doctypes(
limit_start: int | str = 0,
limit_page_length: int | str = DEFAULT_PAGE_LENGTH,
) -> dict[str, Any]:
"""Return role-readable, non-child DocTypes for the authenticated user."""
user = frappe.session.user
if not user or user == "Guest":
frappe.throw(
_("Authentication is required."),
frappe.AuthenticationError,
)
start = max(cint(limit_start), 0)
requested_page_length = (
cint(limit_page_length) or DEFAULT_PAGE_LENGTH
)
page_length = min(
max(requested_page_length, 1),
MAX_PAGE_LENGTH,
)
readable_names = sorted(
set(get_doctypes_with_read(user=user))
)
if not readable_names:
return {
"api_version": "v1",
"user": user,
"permission_basis": "frappe_role_permissions",
"discovery_only": True,
"total": 0,
"limit_start": start,
"limit_page_length": page_length,
"items": [],
}
rows = frappe.get_all(
"DocType",
filters={
"name": ["in", readable_names],
"istable": 0,
},
fields=list(SAFE_DOCTYPE_FIELDS),
order_by="name asc",
)
total = len(rows)
page = rows[start : start + page_length]
items = [
{
"name": row.name,
"module": row.module,
"custom": bool(cint(row.custom)),
"is_single": bool(cint(row.issingle)),
"is_submittable": bool(
cint(row.is_submittable)
),
}
for row in page
]
return {
"api_version": "v1",
"user": user,
"permission_basis": "frappe_role_permissions",
"discovery_only": True,
"total": total,
"limit_start": start,
"limit_page_length": page_length,
"items": items,
}
+1 -1
View File
@@ -29,7 +29,7 @@ class TestCapabilities(FrappeTestCase):
result["capabilities"],
{
"system_information": True,
"doctype_discovery": False,
"doctype_discovery": True,
"document_retrieval": False,
"rag_indexing": False,
"write_operations": False,
+72
View File
@@ -0,0 +1,72 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from enuxia_ai_bridge.api.discovery import doctypes
class TestDoctypeDiscovery(FrappeTestCase):
def setUp(self) -> None:
frappe.set_user("Administrator")
def tearDown(self) -> None:
frappe.set_user("Administrator")
def test_requires_authentication(self) -> None:
frappe.set_user("Guest")
with self.assertRaises(frappe.AuthenticationError):
doctypes()
def test_returns_sorted_paginated_doctypes(self) -> None:
result = doctypes(
limit_start=0,
limit_page_length=10,
)
self.assertEqual(result["api_version"], "v1")
self.assertEqual(result["user"], "Administrator")
self.assertTrue(result["discovery_only"])
self.assertEqual(
result["permission_basis"],
"frappe_role_permissions",
)
self.assertLessEqual(len(result["items"]), 10)
self.assertGreaterEqual(
result["total"],
len(result["items"]),
)
names = [
item["name"]
for item in result["items"]
]
self.assertEqual(names, sorted(names))
for item in result["items"]:
self.assertFalse(
frappe.get_meta(item["name"]).istable
)
def test_caps_page_length(self) -> None:
result = doctypes(
limit_page_length=1000,
)
self.assertEqual(
result["limit_page_length"],
100,
)
self.assertLessEqual(
len(result["items"]),
100,
)
def test_negative_start_becomes_zero(self) -> None:
result = doctypes(
limit_start=-50,
limit_page_length=1,
)
self.assertEqual(result["limit_start"], 0)
View File