Tweak URL designs of new endpoints

This commit is contained in:
Simon Willison 2026-05-25 14:05:26 -07:00
commit f1dd86ebfb
9 changed files with 25 additions and 25 deletions

View file

@ -2745,11 +2745,11 @@ class Datasette:
)
add_route(
QueryInsertView.as_view(self),
r"/(?P<database>[^\/\.]+)/-/queries/-/insert$",
r"/(?P<database>[^\/\.]+)/-/queries/insert$",
)
add_route(
ExecuteWriteAnalyzeView.as_view(self),
r"/(?P<database>[^\/\.]+)/-/execute-write/-/analyze$",
r"/(?P<database>[^\/\.]+)/-/execute-write/analyze$",
)
add_route(
ExecuteWriteView.as_view(self),
@ -2761,7 +2761,7 @@ class Datasette:
)
add_route(
QueryParametersView.as_view(self),
r"/(?P<database>[^\/\.]+)/-/query/-/parameters$",
r"/(?P<database>[^\/\.]+)/-/query/parameters$",
)
add_route(
wrap_view(QueryView, self),

View file

@ -26,7 +26,7 @@
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
{% if allow_execute_sql %}
<form class="sql core" action="{{ urls.database(database) }}/-/query" method="get" data-parameters-url="{{ urls.database(database) }}/-/query/-/parameters">
<form class="sql core" action="{{ urls.database(database) }}/-/query" method="get" data-parameters-url="{{ urls.database(database) }}/-/query/parameters">
<h3>Custom SQL query</h3>
<p class="sql-editor"><textarea id="sql-editor" name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></p>
{% set parameter_names = [] %}

View file

@ -95,7 +95,7 @@
<p class="{% if execution_ok %}message-info{% else %}message-error{% endif %}">{{ execution_message }}{% for link in execution_links %} <a href="{{ link.href }}">{{ link.label }}</a>{% endfor %}</p>
{% endif %}
<form class="sql core" action="{{ urls.database(database) }}/-/execute-write" method="post" data-analyze-url="{{ urls.database(database) }}/-/execute-write/-/analyze">
<form class="sql core" action="{{ urls.database(database) }}/-/execute-write" method="post" data-analyze-url="{{ urls.database(database) }}/-/execute-write/analyze">
{% if write_template_tables %}
<div class="execute-write-template-menu">
<details>

View file

@ -37,7 +37,7 @@
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
<form class="sql core" action="{{ urls.database(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_query_write %}post{% else %}get{% endif %}" data-parameters-url="{{ urls.database(database) }}/-/query/-/parameters">
<form class="sql core" action="{{ urls.database(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_query_write %}post{% else %}get{% endif %}" data-parameters-url="{{ urls.database(database) }}/-/query/parameters">
<h3>Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %}{% if not query_error %}
<span class="show-hide-sql">(<a href="{{ show_hide_link }}">{{ show_hide_text }}</a>)</span>
{% endif %}</h3>

View file

@ -17,7 +17,7 @@
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">Create query</h1>
<form class="sql core" action="{{ urls.database(database) }}/-/queries/-/insert" method="post">
<form class="sql core" action="{{ urls.database(database) }}/-/queries/insert" method="post">
<p><label for="query-name">Name</label> <input id="query-name" name="name" type="text"></p>
<p><label for="query-title">Title</label> <input id="query-title" name="title" type="text"></p>
<p><label for="query-description">Description</label><br><textarea id="query-description" name="description" rows="3"></textarea></p>

View file

@ -525,7 +525,7 @@ Creating saved queries in the UI
Creating saved queries
~~~~~~~~~~~~~~~~~~~~~~
``POST /<database>/-/queries/-/insert`` creates a saved query. This requires ``execute-sql`` and ``insert-query`` for the database.
``POST /<database>/-/queries/insert`` creates a saved query. This requires ``execute-sql`` and ``insert-query`` for the database.
.. _QueryParametersView:
.. _ExecuteWriteView:
@ -534,13 +534,13 @@ Creating saved queries
Executing write SQL
~~~~~~~~~~~~~~~~~~~
``GET /<database>/-/query/-/parameters?sql=...`` returns the named parameters used by a SQL query. This requires ``execute-sql`` for the database.
``GET /<database>/-/query/parameters?sql=...`` returns the named parameters used by a SQL query. This requires ``execute-sql`` for the database.
``GET /<database>/-/execute-write`` displays a form for executing writable SQL. A ``?sql=`` query string pre-populates the form without executing it.
``POST /<database>/-/execute-write`` executes writable SQL. This requires ``execute-write-sql`` for the database plus the relevant table-level write permissions.
``GET /<database>/-/execute-write/-/analyze?sql=...`` returns the derived parameters plus the write operations that SQL would need in order to execute.
``GET /<database>/-/execute-write/analyze?sql=...`` returns the derived parameters plus the write operations that SQL would need in order to execute.
.. _QueryDefinitionView:

View file

@ -211,7 +211,7 @@ JSON endpoints should follow Datasette's existing write API style: use `POST` pl
Endpoints:
- `GET /-/queries` and `GET /{database}/-/queries` show searchable HTML query browsers. `GET /-/queries.json` lists query definitions across every database the actor can view; `GET /{database}/-/queries.json` scopes that list to one database. Both JSON endpoints use cursor pagination with `_next` and `_size`.
- `POST /{database}/-/queries/-/insert` creates a query.
- `POST /{database}/-/queries/insert` creates a query.
- `GET /{database}/{query}/-/definition` returns one query definition without executing it.
- `POST /{database}/{query}/-/update` updates one query.
- `POST /{database}/{query}/-/delete` deletes one query.
@ -388,7 +388,7 @@ The read methods should reconstruct the existing dictionary shape used by query
On `/{database}/-/query`, if the actor has both `execute-sql` and `insert-query`, show a save control for valid read-only SQL. That page already executes read-only arbitrary SQL, so the first UI can stay read-only even though the JSON API can accept writable SQL after `Database.analyze_sql()` validation.
The save form should call `POST /{database}/-/queries/-/insert` and default to `is_published=false`.
The save form should call `POST /{database}/-/queries/insert` and default to `is_published=false`.
If the actor also has `publish-query`, include a publish control. The UI copy should make it clear that publishing allows people without arbitrary SQL permission to run this query.

View file

@ -329,7 +329,7 @@ async def test_query_parameter_form_fields(ds_client):
'<label for="qp1">name</label> <input type="text" id="qp1" name="name" value="" data-parameter-control>'
in response.text
)
assert 'data-parameters-url="/fixtures/-/query/-/parameters"' in response.text
assert 'data-parameters-url="/fixtures/-/query/parameters"' in response.text
assert 'id="sql-parameters-section"' in response.text
assert "setupSqlParameterRefresh" in response.text
response2 = await ds_client.get("/fixtures/-/query?sql=select+:name&name=hello")
@ -344,7 +344,7 @@ async def test_query_parameter_form_fields(ds_client):
async def test_database_page_sql_parameter_refresh_markup(ds_client):
response = await ds_client.get("/fixtures")
assert response.status_code == 200
assert 'data-parameters-url="/fixtures/-/query/-/parameters"' in response.text
assert 'data-parameters-url="/fixtures/-/query/parameters"' in response.text
assert 'id="sql-parameters-section"' in response.text
assert "setupSqlParameterRefresh" in response.text

View file

@ -356,7 +356,7 @@ async def test_query_insert_api_creates_read_only_query():
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/queries/-/insert",
"/data/-/queries/insert",
actor={"id": "root"},
json={
"query": {
@ -568,7 +568,7 @@ async def test_query_insert_api_publish_requires_publish_query():
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/queries/-/insert",
"/data/-/queries/insert",
actor={"id": "writer"},
json={"query": {"name": "public", "sql": "select 1", "is_published": True}},
)
@ -586,7 +586,7 @@ async def test_query_insert_api_creates_writable_query():
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/queries/-/insert",
"/data/-/queries/insert",
actor={"id": "root"},
json={
"query": {
@ -603,7 +603,7 @@ async def test_query_insert_api_creates_writable_query():
assert query["parameters"] == ["name"]
bad_response = await ds.client.post(
"/data/-/queries/-/insert",
"/data/-/queries/insert",
actor={"id": "root"},
json={
"query": {
@ -671,7 +671,7 @@ async def test_query_insert_api_rejects_magic_parameters():
await ds.invoke_startup()
response = await ds.client.post(
"/data/-/queries/-/insert",
"/data/-/queries/insert",
actor={"id": "root"},
json={"query": {"name": "magic", "sql": "select :_actor_id"}},
)
@ -742,7 +742,7 @@ async def test_execute_write_get_prepopulates_without_executing():
assert 'data-sql-template="insert"' in response.text
assert 'data-sql-template="update"' in response.text
assert 'data-sql-template="delete"' in response.text
assert 'data-analyze-url="/data/-/execute-write/-/analyze"' in response.text
assert 'data-analyze-url="/data/-/execute-write/analyze"' in response.text
assert 'addEventListener("paste"' in response.text
assert "setupSqlParameterRefresh" in response.text
assert '<table class="execute-write-analysis">' in response.text
@ -771,12 +771,12 @@ async def test_execute_write_analyze_endpoint_uses_sql_only():
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/execute-write/-/analyze",
"/data/-/execute-write/analyze",
actor={"id": "root"},
params={"sql": "insert into dogs (name) values (:name)"},
)
read_only_response = await ds.client.get(
"/data/-/execute-write/-/analyze",
"/data/-/execute-write/analyze",
actor={"id": "root"},
params={"sql": "select * from dogs where name = :name"},
)
@ -818,19 +818,19 @@ async def test_query_parameters_endpoint_uses_get_sql_only():
await ds.invoke_startup()
response = await ds.client.get(
"/data/-/query/-/parameters",
"/data/-/query/parameters",
actor={"id": "root"},
params={
"sql": "select * from dogs where name = :name and id = :id",
},
)
permission_denied_response = await ds.client.get(
"/data/-/query/-/parameters",
"/data/-/query/parameters",
actor={"id": "not-root"},
params={"sql": "select * from dogs where name = :name"},
)
magic_parameter_response = await ds.client.get(
"/data/-/query/-/parameters",
"/data/-/query/parameters",
actor={"id": "root"},
params={"sql": "select :_actor_id"},
)