mirror of
https://github.com/simonw/datasette.git
synced 2026-09-09 18:14:06 +02:00
GET /db/table/-/insert returns data needed for dialog
As part of making the insert dialog less dependent on the table page.
This commit is contained in:
parent
ad6fe47a95
commit
a76646e3fd
4 changed files with 214 additions and 11 deletions
|
|
@ -682,7 +682,7 @@ function columnFormControlContext(column, isPk, columnType, options) {
|
|||
database: pageData.database || null,
|
||||
table:
|
||||
pageData.table ||
|
||||
(tableInsertData() && tableInsertData().tableName) ||
|
||||
(tableInsertData() && tableInsertData().table_name) ||
|
||||
null,
|
||||
tableUrl: pageData.tableUrl || null,
|
||||
column: column,
|
||||
|
|
@ -1542,7 +1542,7 @@ async function saveRowEditDialog(state) {
|
|||
data && data.rows && data.rows.length ? data.rows[0] : null;
|
||||
var insertedRowId = rowPathFromRowData(
|
||||
insertedRowData,
|
||||
insertData.primaryKeys || [],
|
||||
insertData.primary_keys || [],
|
||||
);
|
||||
state.shouldRestoreFocus = false;
|
||||
if (!insertedRowId) {
|
||||
|
|
@ -1972,8 +1972,8 @@ function openRowInsertDialog(button, manager) {
|
|||
state.dialog.removeAttribute("aria-describedby");
|
||||
setRowDialogTitle(
|
||||
state.title,
|
||||
insertData.tableName
|
||||
? "Insert row into " + insertData.tableName
|
||||
insertData.table_name
|
||||
? "Insert row into " + insertData.table_name
|
||||
: "Insert row",
|
||||
);
|
||||
state.summary.hidden = true;
|
||||
|
|
|
|||
|
|
@ -345,9 +345,9 @@ async def _table_insert_ui(
|
|||
|
||||
return {
|
||||
"path": "{}/-/insert".format(datasette.urls.table(database_name, table_name)),
|
||||
"tableName": table_name,
|
||||
"table_name": table_name,
|
||||
"columns": columns,
|
||||
"primaryKeys": pks,
|
||||
"primary_keys": pks,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -655,6 +655,48 @@ class TableInsertView(BaseView):
|
|||
def __init__(self, datasette):
|
||||
self.ds = datasette
|
||||
|
||||
async def get(self, request):
|
||||
try:
|
||||
resolved = await self.ds.resolve_table(request)
|
||||
except NotFound as e:
|
||||
return _error([e.args[0]], 404)
|
||||
|
||||
db = resolved.db
|
||||
database_name = db.name
|
||||
table_name = resolved.table
|
||||
|
||||
if resolved.is_view:
|
||||
return _error(["Cannot insert rows into a view"], 403)
|
||||
|
||||
if not db.is_mutable:
|
||||
return _error(["Database is immutable"], 403)
|
||||
|
||||
if not await self.ds.allowed(
|
||||
action="insert-row",
|
||||
resource=TableResource(database=database_name, table=table_name),
|
||||
actor=request.actor,
|
||||
):
|
||||
return _error(["Permission denied"], 403)
|
||||
|
||||
pks = await db.primary_keys(table_name)
|
||||
table_insert_ui = await _table_insert_ui(
|
||||
self.ds, request, db, database_name, table_name, resolved.is_view, pks
|
||||
)
|
||||
return Response.json(
|
||||
{
|
||||
"ok": True,
|
||||
"insert_row": table_insert_ui,
|
||||
"table": {
|
||||
"database": database_name,
|
||||
"name": table_name,
|
||||
"url": self.ds.urls.table(database_name, table_name),
|
||||
},
|
||||
"foreign_keys": await _foreign_key_autocomplete_urls(
|
||||
self.ds, request, db, database_name, table_name
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def _validate_data(self, request, db, table_name, pks, upsert):
|
||||
errors = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1530,7 +1530,7 @@ Here's how to serve ``data.db`` with CORS enabled::
|
|||
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`. The token will need to have the specified :ref:`authentication_permissions`.
|
||||
Datasette provides a write API for JSON data. Write operations use ``POST`` and require an authenticated API token, see :ref:`CreateTokenView`. The token will need to have the specified :ref:`authentication_permissions`.
|
||||
|
||||
.. _ExecuteWriteView:
|
||||
|
||||
|
|
@ -1643,6 +1643,46 @@ Inserting rows
|
|||
|
||||
This requires the :ref:`actions_insert_row` permission.
|
||||
|
||||
To return metadata describing the insert form for a table, make a ``GET`` request:
|
||||
|
||||
::
|
||||
|
||||
GET /<database>/<table>/-/insert
|
||||
Authorization: Bearer dstok_<rest-of-token>
|
||||
|
||||
This returns a JSON object describing the table, the columns that can be inserted and any foreign key autocomplete URLs available to the actor:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"insert_row": {
|
||||
"path": "/data/dogs/-/insert",
|
||||
"table_name": "dogs",
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"sqlite_type": "TEXT",
|
||||
"notnull": 1,
|
||||
"default": null,
|
||||
"has_default": false,
|
||||
"is_pk": false,
|
||||
"value_kind": "string",
|
||||
"column_type": null
|
||||
}
|
||||
],
|
||||
"primary_keys": ["id"]
|
||||
},
|
||||
"table": {
|
||||
"database": "data",
|
||||
"name": "dogs",
|
||||
"url": "/data/dogs"
|
||||
},
|
||||
"foreign_keys": {}
|
||||
}
|
||||
|
||||
Integer primary key columns that SQLite can populate automatically are omitted from ``insert_row.columns``. If the actor does not have ``insert-row`` permission this endpoint returns a ``403`` response.
|
||||
|
||||
A single row can be inserted using the ``"row"`` key:
|
||||
|
||||
::
|
||||
|
|
|
|||
|
|
@ -982,8 +982,8 @@ async def test_table_insert_action_button_and_data():
|
|||
|
||||
insert_data = table_data_from_soup(soup)["insertRow"]
|
||||
assert insert_data["path"] == "/data/items/-/insert"
|
||||
assert insert_data["tableName"] == "items"
|
||||
assert insert_data["primaryKeys"] == ["id"]
|
||||
assert insert_data["table_name"] == "items"
|
||||
assert insert_data["primary_keys"] == ["id"]
|
||||
assert [column["name"] for column in insert_data["columns"]] == [
|
||||
"name",
|
||||
"score",
|
||||
|
|
@ -1050,8 +1050,8 @@ async def test_table_insert_action_includes_compound_primary_keys():
|
|||
insert_data = table_data_from_soup(Soup(response.text, "html.parser"))[
|
||||
"insertRow"
|
||||
]
|
||||
assert insert_data["tableName"] == "memberships"
|
||||
assert insert_data["primaryKeys"] == ["account", "username"]
|
||||
assert insert_data["table_name"] == "memberships"
|
||||
assert insert_data["primary_keys"] == ["account", "username"]
|
||||
assert [column["name"] for column in insert_data["columns"]] == [
|
||||
"account",
|
||||
"username",
|
||||
|
|
@ -1066,6 +1066,127 @@ async def test_table_insert_action_includes_compound_primary_keys():
|
|||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_insert_metadata_api():
|
||||
ds = Datasette(
|
||||
[],
|
||||
config={
|
||||
"databases": {
|
||||
"data": {
|
||||
"tables": {
|
||||
"items": {
|
||||
"permissions": {
|
||||
"insert-row": {"id": "root"},
|
||||
},
|
||||
"column_types": {"body": "textarea"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
try:
|
||||
db = ds.add_database(
|
||||
Database(ds, memory_name="test_table_insert_metadata_api"), name="data"
|
||||
)
|
||||
await db.execute_write_script("""
|
||||
create table authors (
|
||||
id integer primary key,
|
||||
name text
|
||||
);
|
||||
create table items (
|
||||
id integer primary key,
|
||||
author_id integer references authors(id),
|
||||
name text not null,
|
||||
score integer default 5,
|
||||
body text
|
||||
);
|
||||
""")
|
||||
response = await ds.client.get("/data/items/-/insert", actor={"id": "root"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"ok": True,
|
||||
"insert_row": {
|
||||
"path": "/data/items/-/insert",
|
||||
"table_name": "items",
|
||||
"columns": [
|
||||
{
|
||||
"name": "author_id",
|
||||
"sqlite_type": "INTEGER",
|
||||
"notnull": 0,
|
||||
"default": None,
|
||||
"has_default": False,
|
||||
"is_pk": False,
|
||||
"value_kind": "number",
|
||||
"column_type": None,
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"sqlite_type": "TEXT",
|
||||
"notnull": 1,
|
||||
"default": None,
|
||||
"has_default": False,
|
||||
"is_pk": False,
|
||||
"value_kind": "string",
|
||||
"column_type": None,
|
||||
},
|
||||
{
|
||||
"name": "score",
|
||||
"sqlite_type": "INTEGER",
|
||||
"notnull": 0,
|
||||
"default": "5",
|
||||
"has_default": True,
|
||||
"is_pk": False,
|
||||
"value_kind": "number",
|
||||
"column_type": None,
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"sqlite_type": "TEXT",
|
||||
"notnull": 0,
|
||||
"default": None,
|
||||
"has_default": False,
|
||||
"is_pk": False,
|
||||
"value_kind": "string",
|
||||
"column_type": {"type": "textarea", "config": None},
|
||||
},
|
||||
],
|
||||
"primary_keys": ["id"],
|
||||
},
|
||||
"table": {
|
||||
"database": "data",
|
||||
"name": "items",
|
||||
"url": "/data/items",
|
||||
},
|
||||
"foreign_keys": {
|
||||
"author_id": "/data/authors/-/autocomplete",
|
||||
},
|
||||
}
|
||||
finally:
|
||||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_insert_metadata_api_requires_insert_permission():
|
||||
ds = Datasette([])
|
||||
try:
|
||||
db = ds.add_database(
|
||||
Database(ds, memory_name="test_table_insert_metadata_permission"),
|
||||
name="data",
|
||||
)
|
||||
await db.execute_write_script("""
|
||||
create table items (id integer primary key, name text);
|
||||
""")
|
||||
response = await ds.client.get("/data/items/-/insert")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"ok": False,
|
||||
"errors": ["Permission denied"],
|
||||
}
|
||||
finally:
|
||||
ds.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_data_includes_foreign_key_autocomplete_urls():
|
||||
ds = Datasette([])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue