From db796771e25dff116db14dab6ff41d9344efe5a8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 13 Nov 2022 20:58:45 -0800 Subject: [PATCH] Example links for API explorer, closes #1871 --- datasette/templates/api_explorer.html | 37 ++++++++- datasette/views/special.py | 113 +++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/datasette/templates/api_explorer.html b/datasette/templates/api_explorer.html index a845482a..f8160e0a 100644 --- a/datasette/templates/api_explorer.html +++ b/datasette/templates/api_explorer.html @@ -22,7 +22,7 @@
- +
@@ -32,7 +32,7 @@
- +
@@ -65,8 +65,11 @@ var getForm = document.getElementById('api-explorer-get'); var output = document.getElementById('output'); var errorList = output.querySelector('.errors'); -// On first load, populate forms from # in URL, if present +// On first load or fragment change populate forms from # in URL, if present if (window.location.hash) { + onFragmentChange(); +} +function onFragmentChange() { var hash = window.location.hash.slice(1); // Treat hash as a foo=bar string and parse it: var params = new URLSearchParams(hash); @@ -82,6 +85,11 @@ if (window.location.hash) { postForm.querySelector('textarea[name="json"]').value = params.get('json'); } } +window.addEventListener('hashchange', () => { + onFragmentChange(); + // Animate scroll to top of page + window.scrollTo({top: 0, behavior: 'smooth'}); +}); // Cause GET and POST regions to toggle each other var getDetails = getForm.closest('details'); @@ -171,4 +179,27 @@ postForm.addEventListener("submit", (ev) => { }); +{% if example_links %} +

API endpoints

+
    + {% for database in example_links %} +
  • Database: {{ database.name }}
  • +
      + {% for link in database.links %} +
    • {{ link.path }} - {{ link.label }}
    • + {% endfor %} + {% for table in database.tables %} +
    • {{ table.name }} +
        + {% for link in table.links %} +
      • {{ link.path }} - {{ link.label }}
      • + {% endfor %} +
      +
    • + {% endfor %} +
    + {% endfor %} +
+{% endif %} + {% endblock %} diff --git a/datasette/views/special.py b/datasette/views/special.py index 79e9da72..468680d3 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -6,6 +6,7 @@ from datasette.permissions import PERMISSIONS from .base import BaseView import secrets import time +import urllib class JsonDataView(BaseView): @@ -275,5 +276,115 @@ class ApiExplorerView(BaseView): name = "api_explorer" has_json_alternate = False + async def example_links(self, request): + databases = [] + for name, db in self.ds.databases.items(): + if name == "_internal": + continue + if not db.is_mutable: + continue + database_visible, _ = await self.ds.check_visibility( + request.actor, + "view-database", + name, + ) + if not database_visible: + continue + tables = [] + table_names = await db.table_names() + for table in table_names: + visible, _ = await self.ds.check_visibility( + request.actor, + "view-table", + (name, table), + ) + if not visible: + continue + table_links = [] + table_links.append( + { + "label": "Get rows for {}".format(table), + "method": "GET", + "path": self.ds.urls.table(name, table, format="json") + + "?_shape=objects".format(name, table), + } + ) + if await self.ds.permission_allowed( + request.actor, "insert-row", (name, table) + ): + pks = await db.primary_keys(table) + table_links.append( + { + "path": self.ds.urls.table(name, table) + "/-/insert", + "method": "POST", + "label": "Insert rows into {}".format(table), + "json": { + "rows": [ + { + column: None + for column in await db.table_columns(table) + if column not in pks + } + ] + }, + } + ) + if await self.ds.permission_allowed( + request.actor, "drop-table", (name, table) + ): + table_links.append( + { + "path": self.ds.urls.table(name, table) + "/-/drop", + "label": "Drop table {}".format(table), + "json": {}, + "method": "POST", + } + ) + tables.append({"name": table, "links": table_links}) + database_links = [] + if await self.ds.permission_allowed(request.actor, "create-table", name): + database_links.append( + { + "path": self.ds.urls.database(name) + "/-/create", + "label": "Create table in {}".format(name), + "json": { + "table": "new_table", + "columns": [ + {"name": "id", "type": "integer"}, + {"name": "name", "type": "text"}, + ], + "pk": "id", + }, + "method": "POST", + } + ) + if database_links or tables: + databases.append( + { + "name": name, + "links": database_links, + "tables": tables, + } + ) + return databases + async def get(self, request): - return await self.render(["api_explorer.html"], request) + def api_path(link): + return "/-/api#{}".format( + urllib.parse.urlencode( + { + key: json.dumps(value, indent=2) if key == "json" else value + for key, value in link.items() + if key in ("path", "method", "json") + } + ) + ) + + return await self.render( + ["api_explorer.html"], + request, + { + "example_links": await self.example_links(request), + "api_path": api_path, + }, + )