mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
/db/table/-/drop API, closes #1874
This commit is contained in:
parent
4f16e14d7a
commit
2865d3956f
6 changed files with 114 additions and 11 deletions
|
|
@ -40,7 +40,7 @@ from .views.special import (
|
||||||
PermissionsDebugView,
|
PermissionsDebugView,
|
||||||
MessagesDebugView,
|
MessagesDebugView,
|
||||||
)
|
)
|
||||||
from .views.table import TableView, TableInsertView
|
from .views.table import TableView, TableInsertView, TableDropView
|
||||||
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
|
||||||
|
|
@ -1276,6 +1276,10 @@ class Datasette:
|
||||||
TableInsertView.as_view(self),
|
TableInsertView.as_view(self),
|
||||||
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/insert$",
|
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/insert$",
|
||||||
)
|
)
|
||||||
|
add_route(
|
||||||
|
TableDropView.as_view(self),
|
||||||
|
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)/-/drop$",
|
||||||
|
)
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -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", "insert-row"):
|
if action in ("permissions-debug", "debug-menu", "insert-row", "drop-table"):
|
||||||
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":
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ LINK_WITH_LABEL = (
|
||||||
LINK_WITH_VALUE = '<a href="{base_url}{database}/{table}/{link_id}">{id}</a>'
|
LINK_WITH_VALUE = '<a href="{base_url}{database}/{table}/{link_id}">{id}</a>'
|
||||||
|
|
||||||
|
|
||||||
|
def _error(messages, status=400):
|
||||||
|
return Response.json({"ok": False, "errors": messages}, status=status)
|
||||||
|
|
||||||
|
|
||||||
class Row:
|
class Row:
|
||||||
def __init__(self, cells):
|
def __init__(self, cells):
|
||||||
self.cells = cells
|
self.cells = cells
|
||||||
|
|
@ -1147,9 +1151,6 @@ class TableInsertView(BaseView):
|
||||||
return rows, errors, extra
|
return rows, errors, extra
|
||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
def _error(messages, status=400):
|
|
||||||
return Response.json({"ok": False, "errors": messages}, status=status)
|
|
||||||
|
|
||||||
database_route = tilde_decode(request.url_vars["database"])
|
database_route = tilde_decode(request.url_vars["database"])
|
||||||
try:
|
try:
|
||||||
db = self.ds.get_database(route=database_route)
|
db = self.ds.get_database(route=database_route)
|
||||||
|
|
@ -1181,7 +1182,8 @@ class TableInsertView(BaseView):
|
||||||
rowids.append(table.insert(row).last_rowid)
|
rowids.append(table.insert(row).last_rowid)
|
||||||
return list(
|
return list(
|
||||||
table.rows_where(
|
table.rows_where(
|
||||||
"rowid in ({})".format(",".join("?" for _ in rowids)), rowids
|
"rowid in ({})".format(",".join("?" for _ in rowids)),
|
||||||
|
rowids,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -1192,3 +1194,33 @@ class TableInsertView(BaseView):
|
||||||
if should_return:
|
if should_return:
|
||||||
result["inserted"] = rows
|
result["inserted"] = rows
|
||||||
return Response.json(result, status=201)
|
return Response.json(result, status=201)
|
||||||
|
|
||||||
|
|
||||||
|
class TableDropView(BaseView):
|
||||||
|
name = "table-drop"
|
||||||
|
|
||||||
|
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:
|
||||||
|
return _error(["Database not found: {}".format(database_route)], 404)
|
||||||
|
database_name = db.name
|
||||||
|
table_name = tilde_decode(request.url_vars["table"])
|
||||||
|
# Table must exist
|
||||||
|
db = self.ds.get_database(database_name)
|
||||||
|
if not await db.table_exists(table_name):
|
||||||
|
return _error(["Table not found: {}".format(table_name)], 404)
|
||||||
|
if not await self.ds.permission_allowed(
|
||||||
|
request.actor, "drop-table", resource=(database_name, table_name)
|
||||||
|
):
|
||||||
|
return _error(["Permission denied"], 403)
|
||||||
|
# Drop table
|
||||||
|
def drop_table(conn):
|
||||||
|
sqlite_utils.Database(conn)[table_name].drop()
|
||||||
|
|
||||||
|
await db.execute_write_fn(drop_table)
|
||||||
|
return Response.json({"ok": True}, status=200)
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,18 @@ Actor is allowed to insert rows into a table.
|
||||||
|
|
||||||
Default *deny*.
|
Default *deny*.
|
||||||
|
|
||||||
|
.. _permissions_drop_table:
|
||||||
|
|
||||||
|
drop-table
|
||||||
|
----------
|
||||||
|
|
||||||
|
Actor is allowed to drop a database 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
|
||||||
|
|
|
||||||
|
|
@ -539,3 +539,20 @@ 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.
|
This will return the same ``"inserted"`` key as the single row example above. There is a small performance penalty for using this option.
|
||||||
|
|
||||||
|
.. _TableDropView:
|
||||||
|
|
||||||
|
Dropping tables
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To drop a table, make a ``POST`` to ``/<database>/<table>/-/drop``. This requires the :ref:`permissions_drop_table` permission.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /<database>/<table>/-/drop
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer dstok_<rest-of-token>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ def ds_write(tmp_path_factory):
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def write_token(ds, actor_id="root"):
|
||||||
|
return "dstok_{}".format(
|
||||||
|
ds.sign(
|
||||||
|
{"a": actor_id, "token": "dstok", "t": int(time.time())}, namespace="token"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_write_row(ds_write):
|
async def test_write_row(ds_write):
|
||||||
token = write_token(ds_write)
|
token = write_token(ds_write)
|
||||||
|
|
@ -188,9 +196,39 @@ async def test_write_row_errors(
|
||||||
assert response.json()["errors"] == expected_errors
|
assert response.json()["errors"] == expected_errors
|
||||||
|
|
||||||
|
|
||||||
def write_token(ds):
|
@pytest.mark.asyncio
|
||||||
return "dstok_{}".format(
|
@pytest.mark.parametrize("scenario", ("no_token", "no_perm", "bad_table", "has_perm"))
|
||||||
ds.sign(
|
async def test_drop_table(ds_write, scenario):
|
||||||
{"a": "root", "token": "dstok", "t": int(time.time())}, namespace="token"
|
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"
|
||||||
|
|
||||||
|
path = "/data/{}/-/drop".format("docs" if scenario != "bad_table" else "bad_table")
|
||||||
|
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")).status_code == 404
|
||||||
|
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 (await ds_write.client.get("/data/docs")).status_code == 200
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue