fix: REST paths
This commit is contained in:
@@ -46,7 +46,7 @@ def register_wasm_extension(app: FastAPI, ext_id: str) -> WasmExtension:
|
|||||||
loaded = load_wasm_extension(ext_id)
|
loaded = load_wasm_extension(ext_id)
|
||||||
_mount_wasm_extension_static(app, loaded)
|
_mount_wasm_extension_static(app, loaded)
|
||||||
_register_wasm_extension_routes(app, loaded)
|
_register_wasm_extension_routes(app, loaded)
|
||||||
_register_wasm_extension_invoke_route(app, loaded)
|
_register_wasm_extension_api_routes(app, loaded)
|
||||||
|
|
||||||
extensions = getattr(app.state, "lnbits_wasm_extensions", {})
|
extensions = getattr(app.state, "lnbits_wasm_extensions", {})
|
||||||
extensions[ext_id] = loaded
|
extensions[ext_id] = loaded
|
||||||
@@ -114,23 +114,29 @@ def _register_wasm_extension_routes(app: FastAPI, extension: WasmExtension) -> N
|
|||||||
_add_wasm_extension_page_route(app, extension, route_path, entrypoint)
|
_add_wasm_extension_page_route(app, extension, route_path, entrypoint)
|
||||||
|
|
||||||
|
|
||||||
def _register_wasm_extension_invoke_route(
|
def _register_wasm_extension_api_routes(app: FastAPI, extension: WasmExtension) -> None:
|
||||||
|
for route_config in extension.config.get("api_routes") or []:
|
||||||
|
_add_wasm_extension_api_route(app, extension, route_config)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wasm_extension_api_route(
|
||||||
app: FastAPI,
|
app: FastAPI,
|
||||||
extension: WasmExtension,
|
extension: WasmExtension,
|
||||||
|
route_config: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
route_path = f"/api/v1/extensions/{extension.id}/invoke/{{export_name}}"
|
method = _wasm_extension_api_method(extension, route_config.get("method"))
|
||||||
if any(getattr(route, "path", None) == route_path for route in app.routes):
|
route_path = _wasm_extension_api_path(extension, route_config.get("path"))
|
||||||
|
export_name = _wasm_extension_api_export(extension, route_config.get("export"))
|
||||||
|
path_params = route_config.get("path_params") or {}
|
||||||
|
|
||||||
|
if _has_route(app, route_path, method):
|
||||||
return
|
return
|
||||||
|
|
||||||
async def invoke_wasm_extension_export(
|
async def invoke_wasm_extension_export(request: Request) -> dict[str, Any]:
|
||||||
export_name: str,
|
|
||||||
request: Request,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
from .wasm import invoke_wasm_extension_export as invoke_export
|
from .wasm import invoke_wasm_extension_export as invoke_export
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_require_http_wasm_export(extension, export_name)
|
payload = await _read_api_payload(request, path_params)
|
||||||
payload = await _read_json_object(request)
|
|
||||||
return await invoke_export(
|
return await invoke_export(
|
||||||
app,
|
app,
|
||||||
extension.id,
|
extension.id,
|
||||||
@@ -147,34 +153,95 @@ def _register_wasm_extension_invoke_route(
|
|||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
route_path,
|
route_path,
|
||||||
invoke_wasm_extension_export,
|
invoke_wasm_extension_export,
|
||||||
methods=["POST"],
|
methods=[method],
|
||||||
name=f"{extension.id}:invoke",
|
name=f"{extension.id}:{method}:{route_path}",
|
||||||
include_in_schema=False,
|
include_in_schema=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_api_payload(
|
||||||
|
request: Request,
|
||||||
|
path_params: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = _read_api_path_params(request, path_params)
|
||||||
|
payload.update(_read_api_query_params(request))
|
||||||
|
if request.method in {"POST", "PUT", "PATCH"}:
|
||||||
|
payload.update(await _read_json_object(request))
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
async def _read_json_object(request: Request) -> dict[str, Any]:
|
async def _read_json_object(request: Request) -> dict[str, Any]:
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
if not body:
|
if not body:
|
||||||
return {}
|
return {}
|
||||||
value = json.loads(body)
|
value = json.loads(body)
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise TypeError("WASM extension invoke payload must be a JSON object.")
|
raise TypeError("WASM extension API payload must be a JSON object.")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _require_http_wasm_export(extension: WasmExtension, export_name: str) -> None:
|
def _read_api_path_params(
|
||||||
|
request: Request,
|
||||||
|
path_params: dict[str, str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
for key, value in request.path_params.items():
|
||||||
|
target = path_params.get(key) or _snake_to_camel(key)
|
||||||
|
payload[target] = value
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _read_api_query_params(request: Request) -> dict[str, Any]:
|
||||||
|
return {_snake_to_camel(key): value for key, value in request.query_params.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _wasm_extension_api_export(extension: WasmExtension, export_name: Any) -> str:
|
||||||
|
if not isinstance(export_name, str) or not export_name:
|
||||||
|
raise ValueError(f"Invalid API export for WASM extension '{extension.id}'.")
|
||||||
|
|
||||||
for export in extension.exports:
|
for export in extension.exports:
|
||||||
if export.get("name") != export_name:
|
if export.get("name") != export_name:
|
||||||
continue
|
continue
|
||||||
if export.get("visibility") in {"public", "authenticated"}:
|
if export.get("visibility") in {"public", "authenticated"}:
|
||||||
return
|
return export_name
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
f"WASM export '{export_name}' is not callable over HTTP."
|
f"WASM export '{export_name}' is not callable over HTTP."
|
||||||
)
|
)
|
||||||
raise KeyError(f"WASM extension '{extension.id}' has no export '{export_name}'.")
|
raise KeyError(f"WASM extension '{extension.id}' has no export '{export_name}'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _wasm_extension_api_method(extension: WasmExtension, method: Any) -> str:
|
||||||
|
if not isinstance(method, str):
|
||||||
|
raise ValueError(f"Invalid API method for WASM extension '{extension.id}'.")
|
||||||
|
method = method.upper()
|
||||||
|
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
||||||
|
raise ValueError(f"Unsupported API method for WASM extension '{extension.id}'.")
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _wasm_extension_api_path(extension: WasmExtension, path: Any) -> str:
|
||||||
|
if not isinstance(path, str) or not path.startswith("/"):
|
||||||
|
raise ValueError(f"Invalid API path for WASM extension '{extension.id}'.")
|
||||||
|
if path == "/":
|
||||||
|
return f"/api/v1/ext/{extension.id}"
|
||||||
|
return f"/api/v1/ext/{extension.id}{path}"
|
||||||
|
|
||||||
|
|
||||||
|
def _has_route(app: FastAPI, route_path: str, method: str) -> bool:
|
||||||
|
for route in app.routes:
|
||||||
|
if getattr(route, "path", None) != route_path:
|
||||||
|
continue
|
||||||
|
methods = getattr(route, "methods", set()) or set()
|
||||||
|
if method in methods:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _snake_to_camel(value: str) -> str:
|
||||||
|
head, *tail = value.split("_")
|
||||||
|
return head + "".join(part.capitalize() for part in tail)
|
||||||
|
|
||||||
|
|
||||||
def _add_wasm_extension_page_route(
|
def _add_wasm_extension_page_route(
|
||||||
app: FastAPI,
|
app: FastAPI,
|
||||||
extension: WasmExtension,
|
extension: WasmExtension,
|
||||||
|
|||||||
Reference in New Issue
Block a user