Skip to content

Commit

Permalink
Link rel=alternate header for tables and rows
Browse files Browse the repository at this point in the history
Also added Access-Control-Expose-Headers: Link to --cors mode.

Closes #1533

Refs simonw/datasette-notebook#2

LL#	metadata.json.1
  • Loading branch information
simonw committed Feb 2, 2022
1 parent 2aa686c commit 3ef47a0
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 6 deletions.
2 changes: 1 addition & 1 deletion datasette/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{% for url in extra_js_urls %}
<script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
{% endfor %}
{% block extra_head %}{% endblock %}
{%- block extra_head %}{% endblock -%}
</head>
<body class="{% block body_class %}{% endblock %}">
<div class="not-footer">
Expand Down
3 changes: 2 additions & 1 deletion datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
{% block title %}{{ database }}: {{ table }}{% endblock %}

{% block extra_head %}
{{ super() }}
{{- super() -}}
<link rel="alternate" type="application/json+datasette" href="{{ alternate_url_json }}">
<style>
@media only screen and (max-width: 576px) {
{% for column in columns %}
Expand Down
3 changes: 2 additions & 1 deletion datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
{% block title %}{{ database }}: {{ table }}: {% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}".format(filtered_table_rows_count) }} row{% if filtered_table_rows_count == 1 %}{% else %}s{% endif %}{% endif %}{% if human_description_en %} {{ human_description_en }}{% endif %}{% endblock %}

{% block extra_head %}
{{ super() }}
{{- super() -}}
<link rel="alternate" type="application/json+datasette" href="{{ alternate_url_json }}">
<script src="{{ urls.static('table.js') }}" defer></script>
<style>
@media only screen and (max-width: 576px) {
Expand Down
1 change: 1 addition & 0 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,3 +1094,4 @@ async def derive_named_parameters(db, sql):
def add_cors_headers(headers):
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Allow-Headers"] = "Authorization"
headers["Access-Control-Expose-Headers"] = "Link"
12 changes: 10 additions & 2 deletions datasette/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,18 @@ async def render(self, templates, request, context=None):
],
},
}
# Hacky cheat to add extra headers
headers = {}
if "_extra_headers" in context:
headers.update(context["_extra_headers"])
return Response.html(
await self.ds.render_template(
template, template_context, request=request, view_name=self.name
)
template,
template_context,
request=request,
view_name=self.name,
),
headers=headers,
)

@classmethod
Expand Down
22 changes: 21 additions & 1 deletion datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
is_url,
path_from_row_pks,
path_with_added_args,
path_with_format,
path_with_removed_args,
path_with_replaced_args,
to_css_class,
Expand Down Expand Up @@ -850,7 +851,12 @@ async def table_actions():
for table_column in table_columns
if table_column not in columns
]
alternate_url_json = self.ds.absolute_url(
request,
self.ds.urls.path(path_with_format(request=request, format="json")),
)
d = {
"alternate_url_json": alternate_url_json,
"table_actions": table_actions,
"use_rowid": use_rowid,
"filters": filters,
Expand Down Expand Up @@ -881,6 +887,11 @@ async def table_actions():
"metadata": metadata,
"view_definition": await db.get_view_definition(table),
"table_definition": await db.get_table_definition(table),
"_extra_headers": {
"Link": '{}; rel="alternate"; type="application/json+datasette"'.format(
alternate_url_json
)
},
}
d.update(extra_context_from_filters)
return d
Expand Down Expand Up @@ -964,8 +975,12 @@ async def template_data():
)
for column in display_columns:
column["sortable"] = False

alternate_url_json = self.ds.absolute_url(
request,
self.ds.urls.path(path_with_format(request=request, format="json")),
)
return {
"alternate_url_json": alternate_url_json,
"foreign_key_tables": await self.foreign_key_tables(
database, table, pk_values
),
Expand All @@ -980,6 +995,11 @@ async def template_data():
.get(database, {})
.get("tables", {})
.get(table, {}),
"_extra_headers": {
"Link": '{}; rel="alternate"; type="application/json+datasette"'.format(
alternate_url_json
)
},
}

data = {
Expand Down
20 changes: 20 additions & 0 deletions docs/json_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ served with the following additional HTTP headers::

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization
Access-Control-Expose-Headers: Link

This means JavaScript running on any domain will be able to make cross-origin
requests to fetch the data.
Expand Down Expand Up @@ -435,3 +436,22 @@ looks like::

The column in the foreign key table that is used for the label can be specified
in ``metadata.json`` - see :ref:`label_columns`.

.. _json_api_discover_alternate:

Discovering the JSON for a page
-------------------------------

The :ref:`table <TableView>` and :ref:`row <RowView>` HTML pages both provide a mechanism for discovering their JSON equivalents using the HTML ``link`` mechanism.

You can find this near the top of those pages, looking like this:

.. code-block:: python
<link rel="alternate"
type="application/json+datasette"
href="https://latest.datasette.io/fixtures/sortable.json">
The JSON URL is also made available in a ``Link`` HTTP header for the page::

Link: https://latest.datasette.io/fixtures/sortable.json; rel="alternate"; type="application/json+datasette"
1 change: 1 addition & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ def test_cors(app_client_with_cors, path, status_code):
assert response.status == status_code
assert response.headers["Access-Control-Allow-Origin"] == "*"
assert response.headers["Access-Control-Allow-Headers"] == "Authorization"
assert response.headers["Access-Control-Expose-Headers"] == "Link"


@pytest.mark.parametrize(
Expand Down
28 changes: 28 additions & 0 deletions tests/test_table_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,3 +1069,31 @@ def test_table_page_title(app_client, path, expected):
response = app_client.get(path)
title = Soup(response.text, "html.parser").find("title").text
assert title == expected


@pytest.mark.parametrize(
"path,expected",
(
(
"/fixtures/table%2Fwith%2Fslashes.csv",
"http://localhost/fixtures/table%2Fwith%2Fslashes.csv?_format=json",
),
("/fixtures/facetable", "http://localhost/fixtures/facetable.json"),
(
"/fixtures/no_primary_key/1",
"http://localhost/fixtures/no_primary_key/1.json",
),
),
)
def test_alternate_url_json(app_client, path, expected):
response = app_client.get(path)
link = response.headers["link"]
assert link == '{}; rel="alternate"; type="application/json+datasette"'.format(
expected
)
assert (
'<link rel="alternate" type="application/json+datasette" href="{}">'.format(
expected
)
in response.text
)

0 comments on commit 3ef47a0

Please sign in to comment.