diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 6177163a..7a9f9115 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -140,7 +140,13 @@ {% if display_rows %}
JSON shape: default, array{% if primary_keys %}, object{% endif %}
JSON shape: + default, + array, + newline-delimited{% if primary_keys %}, + object + {% endif %} +
CSV options: diff --git a/datasette/views/base.py b/datasette/views/base.py index 4b89c975..f4a8afaf 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -432,10 +432,18 @@ class BaseView(RenderMixin): headers = {} if self.ds.cors: headers["Access-Control-Allow-Origin"] = "*" + # Handle _nl option for _shape=array + nl = request.args.get("_nl", "") + if nl and shape == "array": + body = "\n".join(json.dumps(item) for item in data) + content_type = "text/plain" + else: + body = json.dumps(data, cls=CustomJSONEncoder) + content_type = "application/json" r = response.HTTPResponse( - json.dumps(data, cls=CustomJSONEncoder), + body, status=status_code, - content_type="application/json", + content_type=content_type, headers=headers, ) else: diff --git a/docs/advanced_export.png b/docs/advanced_export.png index 90fecbde..d4349fac 100644 Binary files a/docs/advanced_export.png and b/docs/advanced_export.png differ diff --git a/docs/json_api.rst b/docs/json_api.rst index fb1a494e..cd666b54 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -61,11 +61,12 @@ options: * ``?_shape=arrays`` - ``"rows"`` is the default option, shown above * ``?_shape=objects`` - ``"rows"`` is a list of JSON key/value objects -* ``?_shape=array`` - the entire response is an array of objects -* ``?_shape=arrayfirst`` - the entire response is a flat JSON array containing just the first value from each row -* ``?_shape=object`` - the entire response is a JSON object keyed using the primary keys of the rows +* ``?_shape=array`` - an JSON array of objects +* ``?_shape=array&_nl=on`` - a newline-separated list of JSON objects +* ``?_shape=arrayfirst`` - a flat JSON array containing just the first value from each row +* ``?_shape=object`` - a JSON object keyed using the primary keys of the rows -``objects`` looks like this:: +``_shape=objects`` looks like this:: { "database": "sf-trees", @@ -86,7 +87,7 @@ options: ] } -``array`` looks like this:: +``_shape=array`` looks like this:: [ { @@ -103,11 +104,17 @@ options: } ] -``arrayfirst`` looks like this:: +``_shape=array&_nl=on`` looks like this:: + + {"id": 1, "value": "Myoporum laetum :: Myoporum"} + {"id": 2, "value": "Metrosideros excelsa :: New Zealand Xmas Tree"} + {"id": 3, "value": "Pinus radiata :: Monterey Pine"} + +``_shape=arrayfirst`` looks like this:: [1, 2, 3] -``object`` looks like this:: +``_shape=object`` looks like this:: { "1": { @@ -140,6 +147,9 @@ querystring arguments: ``?_shape=SHAPE`` The shape of the JSON to return, documented above. +``?_nl=on`` + When used with ``?_shape=array`` produces newline-delimited JSON objects. + ``?_json=COLUMN1&_json=COLUMN2`` If any of your SQLite columns contain JSON values, you can use one or more ``_json=`` parameters to request that those columns be returned as regular diff --git a/tests/test_api.py b/tests/test_api.py index 03565bdd..8cd1e94e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -9,6 +9,7 @@ from .fixtures import ( # noqa make_app_client, METADATA, ) +import json import pytest import urllib @@ -547,6 +548,27 @@ def test_table_shape_array(app_client): }] == response.json +def test_table_shape_array_nl(app_client): + response = app_client.get( + '/fixtures/simple_primary_key.json?_shape=array&_nl=on' + ) + lines = response.text.split("\n") + results = [json.loads(line) for line in lines] + assert [{ + 'id': '1', + 'content': 'hello', + }, { + 'id': '2', + 'content': 'world', + }, { + 'id': '3', + 'content': '', + }, { + 'id': '4', + 'content': 'RENDER_CELL_DEMO', + }] == results + + def test_table_shape_invalid(app_client): response = app_client.get( '/fixtures/simple_primary_key.json?_shape=invalid' diff --git a/tests/test_html.py b/tests/test_html.py index 233b85a0..9adfe8f2 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -422,6 +422,7 @@ def test_table_csv_json_export_interface(app_client): assert [ "simple_primary_key.json?id__gt=2", "simple_primary_key.json?id__gt=2&_shape=array", + "simple_primary_key.json?id__gt=2&_shape=array&_nl=on", "simple_primary_key.json?id__gt=2&_shape=object" ] == json_links # And the CSV form @@ -796,7 +797,7 @@ def test_advanced_export_box(app_client, path, has_object, has_stream, has_expan assert response.status == 200 soup = Soup(response.body, "html.parser") # JSON shape options - expected_json_shapes = ["default", "array"] + expected_json_shapes = ["default", "array", "newline-delimited"] if has_object: expected_json_shapes.append("object") div = soup.find("div", {"class": "advanced-export"})