mirror of
https://github.com/simonw/datasette.git
synced 2026-10-18 07:17:00 +02:00
Row JSON now returns {"row": {...}}, closes #2612
/db/table/pk.json returns the row as an object in a "row" key instead of a single-item "rows" list. Passing any ?_shape= returns the same shapes as table JSON, so ?_shape=objects gets the previous "rows" list back. This happens in the JSON renderer, so other output renderers still get the "rows" list for row pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaqG8H8ajwxizKXdbGrtve
This commit is contained in:
parent
e8c7b469fa
commit
28184edc52
6 changed files with 97 additions and 16 deletions
|
|
@ -29,7 +29,7 @@ def convert_specific_columns_to_json(rows, columns, json_cols):
|
|||
return new_rows
|
||||
|
||||
|
||||
def json_renderer(request, args, data, error, truncated=None):
|
||||
def json_renderer(request, args, data, error, truncated=None, view_name=None):
|
||||
"""Render a response as JSON"""
|
||||
status_code = 200
|
||||
|
||||
|
|
@ -48,10 +48,13 @@ def json_renderer(request, args, data, error, truncated=None):
|
|||
|
||||
# Deal with the _shape option
|
||||
shape = args.get("_shape", "objects")
|
||||
# Row pages return {"row": {...}} unless a _shape is requested
|
||||
single_row = view_name == "row" and "_shape" not in args
|
||||
# if there's an error, ignore the shape entirely
|
||||
data["ok"] = True
|
||||
if error:
|
||||
shape = "objects"
|
||||
single_row = False
|
||||
status_code = 400
|
||||
data.update(error_body(error, status_code))
|
||||
|
||||
|
|
@ -68,6 +71,13 @@ def json_renderer(request, args, data, error, truncated=None):
|
|||
rows = data.get("rows")
|
||||
if rows and columns and not isinstance(rows[0], dict):
|
||||
data["rows"] = [dict(zip(columns, row)) for row in rows]
|
||||
if single_row and "rows" in data:
|
||||
# Swap "rows" for "row", keeping its position in the output
|
||||
row = data["rows"][0] if data["rows"] else None
|
||||
data = dict(
|
||||
("row", row) if key == "rows" else (key, value)
|
||||
for key, value in data.items()
|
||||
)
|
||||
if shape == "object":
|
||||
shape_error = None
|
||||
if "primary_keys" not in data:
|
||||
|
|
|
|||
|
|
@ -6838,7 +6838,7 @@ async function saveRowEditDialog(state) {
|
|||
}
|
||||
|
||||
function renderRowEditFields(state, data) {
|
||||
var row = data.rows && data.rows.length ? data.rows[0] : null;
|
||||
var row = data.row || null;
|
||||
var columns = data.columns || (row ? Object.keys(row) : []);
|
||||
var primaryKeys = data.primary_keys || [];
|
||||
var columnTypes = data.column_types || {};
|
||||
|
|
|
|||
|
|
@ -647,7 +647,7 @@ class RenderCellExtra(Extra):
|
|||
examples: ClassVar[dict[ExtraScope, ExtraExample | list[ExtraExample]]] = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
value={
|
||||
"rows": [{"id": 4, "content": "RENDER_CELL_DEMO"}],
|
||||
"row": {"id": 4, "content": "RENDER_CELL_DEMO"},
|
||||
"render_cell": [{"content": "<strong>Custom rendered HTML</strong>"}],
|
||||
},
|
||||
note=(
|
||||
|
|
|
|||
|
|
@ -153,6 +153,29 @@ Values for named SQL parameters can be provided as additional query string param
|
|||
|
||||
The response uses the same default representation described above.
|
||||
|
||||
.. _json_api_row:
|
||||
|
||||
Rows
|
||||
----
|
||||
|
||||
Every row in a table has its own JSON URL, see :ref:`RowView`. The row is returned as an object in the ``"row"`` key:
|
||||
|
||||
::
|
||||
|
||||
GET /fixtures/simple_primary_key/1.json
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"row": {
|
||||
"id": 1,
|
||||
"content": "hello"
|
||||
}
|
||||
}
|
||||
|
||||
Pass any :ref:`?_shape= option <json_api_shapes>` to return the row using the same shapes as table JSON instead. ``?_shape=objects`` returns the table default representation, with the row as the only item in the ``"rows"`` list.
|
||||
|
||||
.. _json_api_shapes:
|
||||
|
||||
Different shapes
|
||||
|
|
@ -162,7 +185,7 @@ The ``_shape`` parameter can be used to access alternative formats for the
|
|||
``rows`` key which may be more convenient for your application. There are three
|
||||
options:
|
||||
|
||||
* ``?_shape=objects`` - ``"rows"`` is a list of JSON key/value objects - the default
|
||||
* ``?_shape=objects`` - ``"rows"`` is a list of JSON key/value objects - the default for tables and queries
|
||||
* ``?_shape=arrays`` - ``"rows"`` is a list of lists, where the order of values in each list matches the order of the columns
|
||||
* ``?_shape=array`` - a JSON array of objects - effectively just the ``"rows"`` key from the default representation
|
||||
* ``?_shape=array&_nl=on`` - a newline-separated list of JSON objects
|
||||
|
|
@ -942,12 +965,10 @@ The following extras are available for row JSON responses.
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"id": 4,
|
||||
"content": "RENDER_CELL_DEMO"
|
||||
}
|
||||
],
|
||||
"row": {
|
||||
"id": 4,
|
||||
"content": "RENDER_CELL_DEMO"
|
||||
},
|
||||
"render_cell": [
|
||||
{
|
||||
"content": "<strong>Custom rendered HTML</strong>"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,23 @@ The URL for executing a SQL query now looks like this:
|
|||
|
||||
**This isn't a breaking change.** API calls to the older ``/databasename?sql=...`` endpoint will redirect to the new ``databasename/-/query?sql=...`` endpoint. Upgrading to the new URL is recommended to avoid the overhead of the additional redirect.
|
||||
|
||||
(upgrade_guide_v1_row_json)=
|
||||
### Row JSON returns a single ``"row"`` object
|
||||
|
||||
The JSON for an individual row, such as ``/databasename/tablename/1.json``, previously returned that row as the only item in a ``"rows"`` list. It now returns it as an object in a ``"row"`` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"row": {
|
||||
"id": 1,
|
||||
"content": "hello"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add ``?_shape=objects`` to the URL to get a ``"rows"`` list containing that row instead, or use any other ``?_shape=`` option to get the same shapes as table JSON. See {ref}`json_api_row`.
|
||||
|
||||
(upgrade_guide_v1_metadata)=
|
||||
### Metadata changes
|
||||
|
||||
|
|
|
|||
|
|
@ -363,10 +363,43 @@ async def test_invalid_custom_sql(ds_client):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_row(ds_client):
|
||||
response = await ds_client.get("/fixtures/simple_primary_key/1.json?_shape=objects")
|
||||
response = await ds_client.get("/fixtures/simple_primary_key/1.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
assert response.json()["rows"] == [{"id": 1, "content": "hello"}]
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
assert data["row"] == {"id": 1, "content": "hello"}
|
||||
assert "rows" not in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"shape,expected",
|
||||
(
|
||||
("objects", [{"id": 1, "content": "hello"}]),
|
||||
("arrays", [[1, "hello"]]),
|
||||
("array", [{"id": 1, "content": "hello"}]),
|
||||
("arrayfirst", [1]),
|
||||
("object", {"1": {"id": 1, "content": "hello"}}),
|
||||
),
|
||||
)
|
||||
async def test_row_shape(ds_client, shape, expected):
|
||||
# Any ?_shape= returns the same shapes as the table JSON
|
||||
response = await ds_client.get(
|
||||
f"/fixtures/simple_primary_key/1.json?_shape={shape}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
if shape in ("objects", "arrays"):
|
||||
assert "row" not in data
|
||||
data = data["rows"]
|
||||
assert data == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_row_json_cols(ds_client):
|
||||
response = await ds_client.get("/fixtures/facetable/1.json?_json=tags")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["row"]["tags"] == ["tag1", "tag2"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -615,7 +648,7 @@ async def test_row_extra_render_cell():
|
|||
|
||||
# Verify the response structure
|
||||
assert "render_cell" in data
|
||||
assert "rows" in data
|
||||
assert "row" in data
|
||||
|
||||
# render_cell should be a list with one row (since this is a row page)
|
||||
# Only columns modified by plugins are included (sparse output)
|
||||
|
|
@ -628,8 +661,8 @@ async def test_row_extra_render_cell():
|
|||
# The 'id' column is not included since no plugin modified it
|
||||
assert "id" not in render_cell[0]
|
||||
|
||||
# The regular rows should still contain raw values
|
||||
assert data["rows"] == [{"id": 1, "name": "Alice"}]
|
||||
# The regular row should still contain raw values
|
||||
assert data["row"] == {"id": 1, "name": "Alice"}
|
||||
|
||||
finally:
|
||||
ds.pm.unregister(name="TestRenderCellPlugin")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue