seitime-frappe/frappe/api/v2.py
Akhil Narang 33dbf97db2
fix(document): don't try to overwrite name if a string is already set
This allows us to pass `name` in via API when creating documents.

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2025-05-05 12:24:58 +05:30

211 lines
6 KiB
Python

"""REST API v2
This file defines routes and implementation for REST API.
Note:
- All functions in this file should be treated as "whitelisted" as they are exposed via routes
- None of the functions present here should be called from python code, their location and
internal implementation can change without treating it as "breaking change".
"""
import json
from typing import Any
from werkzeug.routing import Rule
import frappe
import frappe.client
from frappe import _, get_newargs, is_whitelisted
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
from frappe.handler import is_valid_http_method, run_server_script, upload_file
PERMISSION_MAP = {
"GET": "read",
"POST": "write",
}
def handle_rpc_call(method: str, doctype: str | None = None):
from frappe.modules.utils import load_doctype_module
if doctype:
# Expand to run actual method from doctype controller
module = load_doctype_module(doctype)
method = module.__name__ + "." + method
method = frappe.override_whitelisted_method(method)
# via server script
server_script = get_server_script_map().get("_api", {}).get(method)
if server_script:
return run_server_script(server_script)
try:
method = frappe.get_attr(method)
except Exception as e:
frappe.throw(_("Failed to get method {0} with {1}").format(method, e))
is_whitelisted(method)
is_valid_http_method(method)
return frappe.call(method, **frappe.form_dict)
def login():
"""Login happens implicitly, this function doesn't do anything."""
pass
def logout():
frappe.local.login_manager.logout()
frappe.db.commit()
def read_doc(doctype: str, name: str):
doc = frappe.get_doc(doctype, name)
doc.check_permission("read")
doc.apply_fieldlevel_read_permissions()
return doc
def document_list(doctype: str):
if frappe.form_dict.get("fields"):
frappe.form_dict["fields"] = json.loads(frappe.form_dict["fields"])
# set limit of records for frappe.get_list
frappe.form_dict.limit_page_length = frappe.form_dict.limit or 20
# evaluate frappe.get_list
return frappe.call(frappe.client.get_list, doctype, **frappe.form_dict)
def count(doctype: str) -> int:
from frappe.desk.reportview import get_count
frappe.form_dict.doctype = doctype
return get_count()
def create_doc(doctype: str):
data = frappe.form_dict
data.pop("doctype", None)
if (name := data.get("name")) and isinstance(name, str):
frappe.flags.api_name_set = True
return frappe.new_doc(doctype, **data).insert()
def copy_doc(doctype: str, name: str, ignore_no_copy: bool = True):
"""Return a clean copy of the given document that can be modified and posted as a new document."""
doc = frappe.get_doc(doctype, name)
doc.check_permission("read")
doc.apply_fieldlevel_read_permissions()
copy = frappe.copy_doc(doc, ignore_no_copy=ignore_no_copy)
return copy.as_dict(no_private_properties=True, no_nulls=True)
def update_doc(doctype: str, name: str):
data = frappe.form_dict
doc = frappe.get_doc(doctype, name, for_update=True)
data.pop("flags", None)
doc.update(data)
doc.save()
doc.apply_fieldlevel_read_permissions()
# check for child table doctype
if doc.get("parenttype"):
frappe.get_doc(doc.parenttype, doc.parent).save()
return doc
def delete_doc(doctype: str, name: str):
frappe.client.delete_doc(doctype, name)
frappe.response.http_status_code = 202
return "ok"
def get_meta(doctype: str):
frappe.only_for("All")
return frappe.get_meta(doctype)
def execute_doc_method(doctype: str, name: str, method: str | None = None):
"""Get a document from DB and execute method on it.
Use cases:
- Submitting/cancelling document
- Triggering some kind of update on a document
"""
method = method or frappe.form_dict.pop("run_method")
doc = frappe.get_doc(doctype, name)
doc.is_whitelisted(method)
doc.check_permission(PERMISSION_MAP[frappe.request.method])
return doc.run_method(method, **frappe.form_dict)
def run_doc_method(method: str, document: dict[str, Any] | str, kwargs=None):
"""run a whitelisted controller method on in-memory document.
This is useful for building clients that don't necessarily encode all the business logic but
call server side function on object to validate and modify the doc.
The doc CAN exists in DB too and can write to DB as well if method is POST.
"""
if isinstance(document, str):
document = frappe.parse_json(document)
if kwargs is None:
kwargs = {}
doc = frappe.get_doc(document)
doc._original_modified = doc.modified
doc.check_if_latest()
doc.check_permission(PERMISSION_MAP[frappe.request.method])
method_obj = getattr(doc, method)
fn = getattr(method_obj, "__func__", method_obj)
is_whitelisted(fn)
is_valid_http_method(fn)
new_kwargs = get_newargs(fn, kwargs)
response = doc.run_method(method, **new_kwargs)
frappe.response.docs.append(doc) # send modified document and result both.
return response
url_rules = [
# RPC calls
Rule("/method/login", endpoint=login),
Rule("/method/logout", endpoint=logout),
Rule("/method/ping", endpoint=frappe.ping),
Rule("/method/upload_file", endpoint=upload_file),
Rule("/method/<method>", endpoint=handle_rpc_call),
Rule(
"/method/run_doc_method",
methods=["GET", "POST"],
endpoint=lambda: frappe.call(run_doc_method, **frappe.form_dict),
),
Rule("/method/<doctype>/<method>", endpoint=handle_rpc_call),
# Document level APIs
Rule("/document/<doctype>", methods=["GET"], endpoint=document_list),
Rule("/document/<doctype>", methods=["POST"], endpoint=create_doc),
Rule("/document/<doctype>/<path:name>/", methods=["GET"], endpoint=read_doc),
Rule("/document/<doctype>/<path:name>/copy", methods=["GET"], endpoint=copy_doc),
Rule("/document/<doctype>/<path:name>/", methods=["PATCH", "PUT"], endpoint=update_doc),
Rule("/document/<doctype>/<path:name>/", methods=["DELETE"], endpoint=delete_doc),
Rule(
"/document/<doctype>/<path:name>/method/<method>/",
methods=["GET", "POST"],
endpoint=execute_doc_method,
),
# Collection level APIs
Rule("/doctype/<doctype>/meta", methods=["GET"], endpoint=get_meta),
Rule("/doctype/<doctype>/count", methods=["GET"], endpoint=count),
]