mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
New URL design /db/table/-/insert, refs #1851
This commit is contained in:
parent
a2a5dff709
commit
6e788b49ed
4 changed files with 86 additions and 13 deletions
|
|
@ -39,7 +39,7 @@ from .views.special import (
|
||||||
PermissionsDebugView,
|
PermissionsDebugView,
|
||||||
MessagesDebugView,
|
MessagesDebugView,
|
||||||
)
|
)
|
||||||
from .views.table import TableView
|
from .views.table import TableView, TableInsertView
|
||||||
from .views.row import RowView
|
from .views.row import RowView
|
||||||
from .renderer import json_renderer
|
from .renderer import json_renderer
|
||||||
from .url_builder import Urls
|
from .url_builder import Urls
|
||||||
|
|
@ -1262,6 +1262,10 @@ class Datasette:
|
||||||
RowView.as_view(self),
|
RowView.as_view(self),
|
||||||
r"/(?P<database>[^\/\.]+)/(?P<table>[^/]+?)/(?P<pks>[^/]+?)(\.(?P<format>\w+))?$",
|
r"/(?P<database>[^\/\.]+)/(?P<table>[^/]+?)/(?P<pks>[^/]+?)(\.(?P<format>\w+))?$",
|
||||||
)
|
)
|
||||||
|
add_route(
|
||||||
|
TableInsertView.as_view(self),
|
||||||
|
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/insert$",
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
# Compile any strings to regular expressions
|
# Compile any strings to regular expressions
|
||||||
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)
|
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ from datasette.utils import (
|
||||||
)
|
)
|
||||||
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
|
from datasette.utils.asgi import BadRequest, Forbidden, NotFound, Response
|
||||||
from datasette.filters import Filters
|
from datasette.filters import Filters
|
||||||
from .base import DataView, DatasetteError, ureg
|
from .base import BaseView, DataView, DatasetteError, ureg
|
||||||
from .database import QueryView
|
from .database import QueryView
|
||||||
|
|
||||||
LINK_WITH_LABEL = (
|
LINK_WITH_LABEL = (
|
||||||
|
|
@ -1077,3 +1077,70 @@ async def display_columns_and_rows(
|
||||||
}
|
}
|
||||||
columns = [first_column] + columns
|
columns = [first_column] + columns
|
||||||
return columns, cell_rows
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
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
|
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
|
Content-Type: application/json
|
||||||
Authorization: Bearer dstok_<rest-of-token>
|
Authorization: Bearer dstok_<rest-of-token>
|
||||||
{
|
{
|
||||||
"insert": {
|
"row": {
|
||||||
"column1": "value1",
|
"column1": "value1",
|
||||||
"column2": "value2"
|
"column2": "value2"
|
||||||
}
|
}
|
||||||
|
|
@ -487,9 +487,11 @@ If successful, this will return a ``201`` status code and the newly inserted row
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
{
|
||||||
"inserted_row": {
|
"inserted": [
|
||||||
"id": 1,
|
{
|
||||||
"column1": "value1",
|
"id": 1,
|
||||||
"column2": "value2"
|
"column1": "value1",
|
||||||
}
|
"column2": "value2"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ async def test_write_row(ds_write):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
response = await ds_write.client.post(
|
response = await ds_write.client.post(
|
||||||
"/data/docs",
|
"/data/docs/-/insert",
|
||||||
json={"insert": {"title": "Test", "score": 1.0}},
|
json={"row": {"title": "Test", "score": 1.0}},
|
||||||
headers={
|
headers={
|
||||||
"Authorization": "Bearer {}".format(token),
|
"Authorization": "Bearer {}".format(token),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -33,6 +33,6 @@ async def test_write_row(ds_write):
|
||||||
)
|
)
|
||||||
expected_row = {"id": 1, "title": "Test", "score": 1.0}
|
expected_row = {"id": 1, "title": "Test", "score": 1.0}
|
||||||
assert response.status_code == 201
|
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
|
rows = (await ds_write.get_database("data").execute("select * from docs")).rows
|
||||||
assert dict(rows[0]) == expected_row
|
assert dict(rows[0]) == expected_row
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue