From 1b51914a838bb8f2b1cc697e664e743297466451 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 3 Sep 2023 18:16:31 +0530 Subject: [PATCH] refactor: create two API versions --- frappe/api/__init__.py | 17 ++++-- frappe/api/v1.py | 16 +++--- frappe/api/v2.py | 125 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 frappe/api/v2.py diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index c49cbd6c18..e2de93111b 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -5,7 +5,7 @@ import binascii from urllib.parse import urlencode, urlparse from werkzeug.exceptions import NotFound -from werkzeug.routing import Map +from werkzeug.routing import Map, Submount from werkzeug.wrappers import Request import frappe @@ -149,9 +149,14 @@ def validate_auth_via_hooks(): # Merge all API version routing rules from frappe.api.v1 import url_rules as v1_rules +from frappe.api.v2 import url_rules as v2_rules -url_rules = [ - *v1_rules, -] - -API_URL_MAP = Map(url_rules) +API_URL_MAP = Map( + [ + # V1 routes + Submount("/api", v1_rules), + Submount("/api/v1", v1_rules), + Submount("/api/v2", v2_rules), + ], + strict_slashes=False, # Allows skipping trailing slashes +) diff --git a/frappe/api/v1.py b/frappe/api/v1.py index cf2932e869..0d1de01da7 100644 --- a/frappe/api/v1.py +++ b/frappe/api/v1.py @@ -121,13 +121,11 @@ def get_request_form_data(): url_rules = [ - Rule("/api/method/", endpoint=handle_rpc_call), - Rule("/api/resource/", methods=["GET"], endpoint=get_doc_list), - Rule("/api/resource/", methods=["POST"], endpoint=create_doc), - Rule("/api/resource//", methods=["GET"], endpoint=read_doc), - Rule("/api/resource//", methods=["PUT"], endpoint=update_doc), - Rule("/api/resource//", methods=["DELETE"], endpoint=delete_doc), - Rule( - "/api/resource//", methods=["POST"], endpoint=execute_doc_method - ), + Rule("/method/", endpoint=handle_rpc_call), + Rule("/resource/", methods=["GET"], endpoint=get_doc_list), + Rule("/resource/", methods=["POST"], endpoint=create_doc), + Rule("/resource//", methods=["GET"], endpoint=read_doc), + Rule("/resource//", methods=["PUT"], endpoint=update_doc), + Rule("/resource//", methods=["DELETE"], endpoint=delete_doc), + Rule("/resource//", methods=["POST"], endpoint=execute_doc_method), ] diff --git a/frappe/api/v2.py b/frappe/api/v2.py new file mode 100644 index 0000000000..1fbb74693d --- /dev/null +++ b/frappe/api/v2.py @@ -0,0 +1,125 @@ +import json + +from werkzeug.routing import Rule + +import frappe +from frappe import _ +from frappe.utils.data import sbool + + +def handle_rpc_call(method: str): + # TODO: inline this weird circular calls + frappe.local.form_dict.cmd = method + return frappe.handler.handle() + + +def get_doc_list(doctype: str): + if frappe.local.form_dict.get("fields"): + frappe.local.form_dict["fields"] = json.loads(frappe.local.form_dict["fields"]) + + # set limit of records for frappe.get_list + frappe.local.form_dict.setdefault( + "limit_page_length", + frappe.local.form_dict.limit or frappe.local.form_dict.limit_page_length or 20, + ) + + # convert strings to native types - only as_dict and debug accept bool + for param in ["as_dict", "debug"]: + param_val = frappe.local.form_dict.get(param) + if param_val is not None: + frappe.local.form_dict[param] = sbool(param_val) + + # evaluate frappe.get_list + data = frappe.call(frappe.client.get_list, doctype, **frappe.local.form_dict) + + # set frappe.get_list result to response + frappe.local.response.update({"data": data}) + + +def create_doc(doctype: str): + data = get_request_form_data() + data.update({"doctype": doctype}) + + # insert document from request data + doc = frappe.get_doc(data).insert() + + # set response data + frappe.local.response.update({"data": doc.as_dict()}) + + # commit for POST requests + frappe.db.commit() + + +def read_doc(doctype: str, name: str): + doc = frappe.get_doc(doctype, name) + if not doc.has_permission("read"): + raise frappe.PermissionError + doc.apply_fieldlevel_read_permissions() + frappe.local.response.update({"data": doc}) + + +def update_doc(doctype: str, name: str): + data = get_request_form_data() + + doc = frappe.get_doc(doctype, name, for_update=True) + + if "flags" in data: + del data["flags"] + + # Not checking permissions here because it's checked in doc.save + doc.update(data) + + frappe.local.response.update({"data": doc.save().as_dict()}) + + # check for child table doctype + if doc.get("parenttype"): + frappe.get_doc(doc.parenttype, doc.parent).save() + frappe.db.commit() + + +def delete_doc(doctype: str, name: str): + # Not checking permissions here because it's checked in delete_doc + frappe.delete_doc(doctype, name, ignore_missing=False) + frappe.local.response.http_status_code = 202 + frappe.local.response.message = "ok" + frappe.db.commit() + + +def execute_doc_method(doctype: str, name: str, method: str | None = None): + method = method or frappe.local.form_dict.pop("run_method") + doc = frappe.get_doc(doctype, name) + doc.is_whitelisted(method) + + if frappe.local.request.method == "GET": + if not doc.has_permission("read"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) + + elif frappe.local.request.method == "POST": + if not doc.has_permission("write"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + frappe.local.response.update({"data": doc.run_method(method, **frappe.local.form_dict)}) + frappe.db.commit() + + +def get_request_form_data(): + if frappe.local.form_dict.data is None: + data = frappe.safe_decode(frappe.local.request.get_data()) + else: + data = frappe.local.form_dict.data + + try: + return frappe.parse_json(data) + except ValueError: + return frappe.local.form_dict + + +url_rules = [ + Rule("/method/", endpoint=handle_rpc_call), + Rule("/resource/", methods=["GET"], endpoint=get_doc_list), + Rule("/resource/", methods=["POST"], endpoint=create_doc), + Rule("/resource//", methods=["GET"], endpoint=read_doc), + Rule("/resource//", methods=["PUT"], endpoint=update_doc), + Rule("/resource//", methods=["DELETE"], endpoint=delete_doc), +]