New URL design /db/table/-/insert, refs #1851

This commit is contained in:
Simon Willison 2022-10-27 13:17:18 -07:00
commit 6e788b49ed
4 changed files with 86 additions and 13 deletions

View file

@ -39,7 +39,7 @@ from .views.special import (
PermissionsDebugView,
MessagesDebugView,
)
from .views.table import TableView
from .views.table import TableView, TableInsertView
from .views.row import RowView
from .renderer import json_renderer
from .url_builder import Urls
@ -1262,6 +1262,10 @@ class Datasette:
RowView.as_view(self),
r"/(?P<database>[^\/\.]+)/(?P<table>[^/]+?)/(?P<pks>[^/]+?)(\.(?P<format>\w+))?$",
)
add_route(
TableInsertView.as_view(self),
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/insert$",
)
return [
# Compile any strings to regular expressions
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)

View file

@ -30,7 +30,7 @@ from datasette.utils import (
)
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
from datasette.filters import Filters
from .base import DataView, DatasetteError, ureg
from .base import BaseView, DataView, DatasetteError, ureg
from .database import QueryView
LINK_WITH_LABEL = (
@ -1077,3 +1077,70 @@ async def display_columns_and_rows(
}
columns = [first_column] + columns
return columns, cell_rows
class TableInsertView(BaseView):
name = "table-insert"
def __init__(self, datasette):
self.ds = datasette
async def post(self, request):
database_route = tilde_decode(request.url_vars["database"])
try:
db = self.ds.get_database(route=database_route)
except KeyError:
raise NotFound("Database not found: {}".format(database_route))
database_name = db.name
table_name = tilde_decode(request.url_vars["table"])
# Table must exist (may handle table creation in the future)
db = self.ds.get_database(database_name)
if not await db.table_exists(table_name):
raise NotFound("Table not found: {}".format(table_name))
# Must have insert-row permission
if not await self.ds.permission_allowed(
request.actor, "insert-row", resource=(database_name, table_name)
):
raise Forbidden("Permission denied")
if request.headers.get("content-type") != "application/json":
# TODO: handle form-encoded data
raise BadRequest("Must send JSON data")
data = json.loads(await request.post_body())
if "row" not in data:
raise BadRequest('Must send a "row" key containing a dictionary')
row = data["row"]
if not isinstance(row, dict):
raise BadRequest("row must be a dictionary")
# Verify all columns exist
columns = await db.table_columns(table_name)
pks = await db.primary_keys(table_name)
for key in row:
if key not in columns:
raise BadRequest("Column not found: {}".format(key))
if key in pks:
raise BadRequest(
"Cannot insert into primary key column: {}".format(key)
)
# Perform the insert
sql = "INSERT INTO [{table}] ({columns}) VALUES ({values})".format(
table=escape_sqlite(table_name),
columns=", ".join(escape_sqlite(c) for c in row),
values=", ".join("?" for c in row),
)
cursor = await db.execute_write(sql, list(row.values()))
# Return the new row
rowid = cursor.lastrowid
new_row = (
await db.execute(
"SELECT * FROM [{table}] WHERE rowid = ?".format(
table=escape_sqlite(table_name)
),
[rowid],
)
).first()
return Response.json(
{
"inserted": [dict(new_row)],
},
status=201,
)

View file

@ -463,7 +463,7 @@ The JSON write API
Datasette provides a write API for JSON data. This is a POST-only API that requires an authenticated API token, see :ref:`CreateTokenView`.
.. _json_api_write_insert_row:
.. _TableInsertView:
Inserting a single row
~~~~~~~~~~~~~~~~~~~~~~
@ -472,11 +472,11 @@ This requires the :ref:`permissions_insert_row` permission.
::
POST /<database>/<table>
POST /<database>/<table>/-/insert
Content-Type: application/json
Authorization: Bearer dstok_<rest-of-token>
{
"insert": {
"row": {
"column1": "value1",
"column2": "value2"
}
@ -487,9 +487,11 @@ If successful, this will return a ``201`` status code and the newly inserted row
.. code-block:: json
{
"inserted_row": {
"id": 1,
"column1": "value1",
"column2": "value2"
}
"inserted": [
{
"id": 1,
"column1": "value1",
"column2": "value2"
}
]
}

View file

@ -24,8 +24,8 @@ async def test_write_row(ds_write):
)
)
response = await ds_write.client.post(
"/data/docs",
json={"insert": {"title": "Test", "score": 1.0}},
"/data/docs/-/insert",
json={"row": {"title": "Test", "score": 1.0}},
headers={
"Authorization": "Bearer {}".format(token),
"Content-Type": "application/json",
@ -33,6 +33,6 @@ async def test_write_row(ds_write):
)
expected_row = {"id": 1, "title": "Test", "score": 1.0}
assert response.status_code == 201
assert response.json()["inserted_row"] == expected_row
assert response.json()["inserted"] == [expected_row]
rows = (await ds_write.get_database("data").execute("select * from docs")).rows
assert dict(rows[0]) == expected_row