kopia lustrzana https://github.com/simonw/datasette
Initial attempt at /db/table/row/-/delete, refs #1864
rodzic
2865d3956f
commit
00632ded30
|
@ -41,7 +41,7 @@ from .views.special import (
|
|||
MessagesDebugView,
|
||||
)
|
||||
from .views.table import TableView, TableInsertView, TableDropView
|
||||
from .views.row import RowView
|
||||
from .views.row import RowView, RowDeleteView
|
||||
from .renderer import json_renderer
|
||||
from .url_builder import Urls
|
||||
from .database import Database, QueryInterrupted
|
||||
|
@ -1280,6 +1280,10 @@ class Datasette:
|
|||
TableDropView.as_view(self),
|
||||
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/drop$",
|
||||
)
|
||||
add_route(
|
||||
RowDeleteView.as_view(self),
|
||||
r"/(?P<database>[^\/\.]+)/(?P<table>[^/]+?)/(?P<pks>[^/]+?)/-/delete$",
|
||||
)
|
||||
return [
|
||||
# Compile any strings to regular expressions
|
||||
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)
|
||||
|
|
|
@ -9,7 +9,13 @@ import time
|
|||
@hookimpl(tryfirst=True)
|
||||
def permission_allowed(datasette, actor, action, resource):
|
||||
async def inner():
|
||||
if action in ("permissions-debug", "debug-menu", "insert-row", "drop-table"):
|
||||
if action in (
|
||||
"permissions-debug",
|
||||
"debug-menu",
|
||||
"insert-row",
|
||||
"drop-table",
|
||||
"delete-row",
|
||||
):
|
||||
if actor and actor.get("id") == "root":
|
||||
return True
|
||||
elif action == "view-instance":
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
from datasette.utils.asgi import NotFound, Forbidden
|
||||
from datasette.utils.asgi import NotFound, Forbidden, Response
|
||||
from datasette.database import QueryInterrupted
|
||||
from .base import DataView
|
||||
from .base import DataView, BaseView
|
||||
from datasette.utils import (
|
||||
tilde_decode,
|
||||
urlsafe_components,
|
||||
to_css_class,
|
||||
escape_sqlite,
|
||||
)
|
||||
import sqlite_utils
|
||||
from .table import _sql_params_pks, display_columns_and_rows
|
||||
|
||||
|
||||
def _error(messages, status=400):
|
||||
return Response.json({"ok": False, "errors": messages}, status=status)
|
||||
|
||||
|
||||
class RowView(DataView):
|
||||
name = "row"
|
||||
|
||||
|
@ -146,3 +151,43 @@ class RowView(DataView):
|
|||
)
|
||||
foreign_key_tables.append({**fk, **{"count": count, "link": link}})
|
||||
return foreign_key_tables
|
||||
|
||||
|
||||
class RowDeleteView(BaseView):
|
||||
name = "row-delete"
|
||||
|
||||
def __init__(self, datasette):
|
||||
self.ds = datasette
|
||||
|
||||
async def post(self, request):
|
||||
database_route = tilde_decode(request.url_vars["database"])
|
||||
table = tilde_decode(request.url_vars["table"])
|
||||
try:
|
||||
db = self.ds.get_database(route=database_route)
|
||||
except KeyError:
|
||||
return _error(["Database not found: {}".format(database_route)], 404)
|
||||
|
||||
database_name = db.name
|
||||
if not await db.table_exists(table):
|
||||
return _error(["Table not found: {}".format(table)], 404)
|
||||
|
||||
pk_values = urlsafe_components(request.url_vars["pks"])
|
||||
|
||||
sql, params, pks = await _sql_params_pks(db, table, pk_values)
|
||||
results = await db.execute(sql, params, truncate=True)
|
||||
rows = list(results.rows)
|
||||
if not rows:
|
||||
return _error([f"Record not found: {pk_values}"], 404)
|
||||
|
||||
# Ensure user has permission to delete this row
|
||||
if not await self.ds.permission_allowed(
|
||||
request.actor, "delete-row", resource=(database_name, table)
|
||||
):
|
||||
return _error(["Permission denied"], 403)
|
||||
|
||||
# Delete table
|
||||
def delete_row(conn):
|
||||
sqlite_utils.Database(conn)[table].delete(pk_values)
|
||||
|
||||
await db.execute_write_fn(delete_row)
|
||||
return Response.json({"ok": True}, status=200)
|
||||
|
|
|
@ -559,6 +559,18 @@ Actor is allowed to insert rows into a table.
|
|||
|
||||
Default *deny*.
|
||||
|
||||
.. _permissions_delete_row:
|
||||
|
||||
delete-row
|
||||
----------
|
||||
|
||||
Actor is allowed to delete rows from a table.
|
||||
|
||||
``resource`` - tuple: (string, string)
|
||||
The name of the database, then the name of the table
|
||||
|
||||
Default *deny*.
|
||||
|
||||
.. _permissions_drop_table:
|
||||
|
||||
drop-table
|
||||
|
|
|
@ -540,6 +540,25 @@ To return the newly inserted rows, add the ``"return_rows": true`` key to the re
|
|||
|
||||
This will return the same ``"inserted"`` key as the single row example above. There is a small performance penalty for using this option.
|
||||
|
||||
.. _RowDeleteView:
|
||||
|
||||
Deleting rows
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
To delete a row, make a ``POST`` to ``/<database>/<table>/<row-pks>/-/delete``. This requires the :ref:`permissions_delete_row` permission.
|
||||
|
||||
::
|
||||
|
||||
POST /<database>/<table>/<row-pks>/-/delete
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer dstok_<rest-of-token>
|
||||
|
||||
``<row-pks>`` here is the :ref:`tilde-encoded <internals_tilde_encoding>` primary key value of the row to delete - or a comma-separated list of primary key values if the table has a composite primary key.
|
||||
|
||||
If successful, this will return a ``200`` status code and a ``{"ok": true}`` response body.
|
||||
|
||||
Any errors will return ``{"errors": ["... descriptive message ..."], "ok": false}``, and a ``400`` status code for a bad input or a ``403`` status code for an authentication or permission error.
|
||||
|
||||
.. _TableDropView:
|
||||
|
||||
Dropping tables
|
||||
|
|
|
@ -196,6 +196,60 @@ async def test_write_row_errors(
|
|||
assert response.json()["errors"] == expected_errors
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("scenario", ("no_token", "no_perm", "bad_table", "has_perm"))
|
||||
async def test_delete_row(ds_write, scenario):
|
||||
if scenario == "no_token":
|
||||
token = "bad_token"
|
||||
elif scenario == "no_perm":
|
||||
token = write_token(ds_write, actor_id="not-root")
|
||||
else:
|
||||
token = write_token(ds_write)
|
||||
should_work = scenario == "has_perm"
|
||||
|
||||
# Insert a row
|
||||
insert_response = await ds_write.client.post(
|
||||
"/data/docs/-/insert",
|
||||
json={"row": {"title": "Row one", "score": 1.0}, "return_rows": True},
|
||||
headers={
|
||||
"Authorization": "Bearer {}".format(write_token(ds_write)),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
assert insert_response.status_code == 201
|
||||
pk = insert_response.json()["inserted"][0]["id"]
|
||||
|
||||
path = "/data/{}/{}/-/delete".format(
|
||||
"docs" if scenario != "bad_table" else "bad_table", pk
|
||||
)
|
||||
response = await ds_write.client.post(
|
||||
path,
|
||||
headers={
|
||||
"Authorization": "Bearer {}".format(token),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
if should_work:
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"ok": True}
|
||||
assert (await ds_write.client.get("/data/docs.json?_shape=array")).json() == []
|
||||
else:
|
||||
assert (
|
||||
response.status_code == 403
|
||||
if scenario in ("no_token", "bad_token")
|
||||
else 404
|
||||
)
|
||||
assert response.json()["ok"] is False
|
||||
assert (
|
||||
response.json()["errors"] == ["Permission denied"]
|
||||
if scenario == "no_token"
|
||||
else ["Table not found: bad_table"]
|
||||
)
|
||||
assert (
|
||||
len((await ds_write.client.get("/data/docs.json?_shape=array")).json()) == 1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("scenario", ("no_token", "no_perm", "bad_table", "has_perm"))
|
||||
async def test_drop_table(ds_write, scenario):
|
||||
|
|
Ładowanie…
Reference in New Issue