mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
First draft of insert row write API, refs #1851
This commit is contained in:
parent
382a871583
commit
51c436fed2
5 changed files with 119 additions and 11 deletions
|
|
@ -9,7 +9,7 @@ import time
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
def permission_allowed(datasette, actor, action, resource):
|
def permission_allowed(datasette, actor, action, resource):
|
||||||
async def inner():
|
async def inner():
|
||||||
if action in ("permissions-debug", "debug-menu"):
|
if action in ("permissions-debug", "debug-menu", "insert-row"):
|
||||||
if actor and actor.get("id") == "root":
|
if actor and actor.get("id") == "root":
|
||||||
return True
|
return True
|
||||||
elif action == "view-instance":
|
elif action == "view-instance":
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ from datasette.utils import (
|
||||||
urlsafe_components,
|
urlsafe_components,
|
||||||
value_as_boolean,
|
value_as_boolean,
|
||||||
)
|
)
|
||||||
from datasette.utils.asgi import BadRequest, Forbidden, NotFound
|
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 DataView, DatasetteError, ureg
|
||||||
from .database import QueryView
|
from .database import QueryView
|
||||||
|
|
@ -103,15 +103,71 @@ class TableView(DataView):
|
||||||
canned_query = await self.ds.get_canned_query(
|
canned_query = await self.ds.get_canned_query(
|
||||||
database_name, table_name, request.actor
|
database_name, table_name, request.actor
|
||||||
)
|
)
|
||||||
assert canned_query, "You may only POST to a canned query"
|
if canned_query:
|
||||||
return await QueryView(self.ds).data(
|
return await QueryView(self.ds).data(
|
||||||
request,
|
request,
|
||||||
canned_query["sql"],
|
canned_query["sql"],
|
||||||
metadata=canned_query,
|
metadata=canned_query,
|
||||||
editable=False,
|
editable=False,
|
||||||
canned_query=table_name,
|
canned_query=table_name,
|
||||||
named_parameters=canned_query.get("params"),
|
named_parameters=canned_query.get("params"),
|
||||||
write=bool(canned_query.get("write")),
|
write=bool(canned_query.get("write")),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Handle POST to a table
|
||||||
|
return await self.table_post(request, database_name, table_name)
|
||||||
|
|
||||||
|
async def table_post(self, request, database_name, table_name):
|
||||||
|
# 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 "row" data')
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
"row": dict(new_row),
|
||||||
|
},
|
||||||
|
status=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def columns_to_select(self, table_columns, pks, request):
|
async def columns_to_select(self, table_columns, pks, request):
|
||||||
|
|
|
||||||
|
|
@ -547,6 +547,18 @@ Actor is allowed to view (and execute) a :ref:`canned query <canned_queries>` pa
|
||||||
|
|
||||||
Default *allow*.
|
Default *allow*.
|
||||||
|
|
||||||
|
.. _permissions_insert_row:
|
||||||
|
|
||||||
|
insert-row
|
||||||
|
----------
|
||||||
|
|
||||||
|
Actor is allowed to insert rows into a table.
|
||||||
|
|
||||||
|
``resource`` - tuple: (string, string)
|
||||||
|
The name of the database, then the name of the table
|
||||||
|
|
||||||
|
Default *deny*.
|
||||||
|
|
||||||
.. _permissions_execute_sql:
|
.. _permissions_execute_sql:
|
||||||
|
|
||||||
execute-sql
|
execute-sql
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,8 @@ These can be passed to ``datasette serve`` using ``datasette serve --setting nam
|
||||||
database files (default=True)
|
database files (default=True)
|
||||||
allow_signed_tokens Allow users to create and use signed API tokens
|
allow_signed_tokens Allow users to create and use signed API tokens
|
||||||
(default=True)
|
(default=True)
|
||||||
|
max_signed_tokens_ttl Maximum allowed expiry time for signed API tokens
|
||||||
|
(default=0)
|
||||||
suggest_facets Calculate and display suggested facets
|
suggest_facets Calculate and display suggested facets
|
||||||
(default=True)
|
(default=True)
|
||||||
default_cache_ttl Default HTTP cache TTL (used in Cache-Control:
|
default_cache_ttl Default HTTP cache TTL (used in Cache-Control:
|
||||||
|
|
|
||||||
|
|
@ -455,3 +455,41 @@ You can find this near the top of the source code of those pages, looking like t
|
||||||
The JSON URL is also made available in a ``Link`` HTTP header for the page::
|
The JSON URL is also made available in a ``Link`` HTTP header for the page::
|
||||||
|
|
||||||
Link: https://latest.datasette.io/fixtures/sortable.json; rel="alternate"; type="application/json+datasette"
|
Link: https://latest.datasette.io/fixtures/sortable.json; rel="alternate"; type="application/json+datasette"
|
||||||
|
|
||||||
|
.. _json_api_write:
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
Inserting a single row
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This requires the :ref:`permissions_insert_row` permission.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /<database>/<table>
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer dstok_<rest-of-token>
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"column1": "value1",
|
||||||
|
"column2": "value2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If successful, this will return a ``201`` status code and the newly inserted row, for example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"id": 1,
|
||||||
|
"column1": "value1",
|
||||||
|
"column2": "value2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue