mirror of
https://github.com/simonw/datasette.git
synced 2026-06-11 19:46:58 +02:00
Make filters, actions and display_rows extras internal
These three extras return values that exist for the HTML templates - a Filters instance, an async function and markupsafe/sqlite3.Row data - so requesting them on a .json page returned a 500 serialization error, while the generated documentation and ?_extra=extras both advertised them as API surface. They are now public=False: ignored like any unknown name on JSON requests, omitted from the docs and the extras list, and still resolved for the HTML view via the new include_internal flag on ExtraRegistry.resolve(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8f888515b6
commit
b635dc53f4
5 changed files with 44 additions and 21 deletions
|
|
@ -89,7 +89,7 @@ class ExtraRegistry:
|
|||
def public_classes_for_scope(self, scope):
|
||||
return self.classes_for_scope(scope, include_internal=False)
|
||||
|
||||
async def resolve(self, requested, context, scope):
|
||||
async def resolve(self, requested, context, scope, include_internal=False):
|
||||
registry = Registry()
|
||||
|
||||
async def context_provider():
|
||||
|
|
@ -100,15 +100,14 @@ class ExtraRegistry:
|
|||
for cls in self.classes_for_scope(scope):
|
||||
registry.register(cls().resolve, name=cls.key())
|
||||
|
||||
public_names = {cls.key() for cls in self.public_classes_for_scope(scope)}
|
||||
requested_public_names = [
|
||||
name
|
||||
for name in requested
|
||||
if name in public_names and name in registry._registry
|
||||
]
|
||||
resolved = await registry.resolve_multi(requested_public_names)
|
||||
allowed_names = {
|
||||
cls.key()
|
||||
for cls in self.classes_for_scope(scope, include_internal=include_internal)
|
||||
}
|
||||
requested_names = [name for name in requested if name in allowed_names]
|
||||
resolved = await registry.resolve_multi(requested_names)
|
||||
return {
|
||||
name: resolved[name] for name in requested_public_names if name in resolved
|
||||
name: resolved[name] for name in requested_names if name in resolved
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1518,7 +1518,14 @@ async def table_view_data(
|
|||
"ok": True,
|
||||
"next": next_value and str(next_value) or None,
|
||||
}
|
||||
data.update(await resolve_table_extras(extras, table_extra_context))
|
||||
data.update(
|
||||
await resolve_table_extras(
|
||||
extras,
|
||||
table_extra_context,
|
||||
# The HTML view needs extras that are not JSON serializable
|
||||
include_internal=bool(extra_extras),
|
||||
)
|
||||
)
|
||||
raw_sqlite_rows = rows[:page_size]
|
||||
# Apply transform_value for columns with assigned types
|
||||
ct_map = await datasette.get_column_types(database_name, table_name)
|
||||
|
|
|
|||
|
|
@ -333,6 +333,8 @@ class PrimaryKeysExtra(Extra):
|
|||
class ActionsExtra(Extra):
|
||||
description = "Table or view actions made available by plugin hooks"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
# Returns an async function for the HTML templates - not JSON serializable
|
||||
public = False
|
||||
|
||||
async def resolve(self, context):
|
||||
async def actions():
|
||||
|
|
@ -476,6 +478,8 @@ class DisplayColumnsExtra(Extra):
|
|||
class DisplayRowsExtra(Extra):
|
||||
description = "Row data formatted for the HTML table display"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
# Contains markupsafe/sqlite3.Row values - not JSON serializable
|
||||
public = False
|
||||
|
||||
async def resolve(self, context, display_columns_and_rows):
|
||||
return display_columns_and_rows["rows"]
|
||||
|
|
@ -772,6 +776,8 @@ class FormHiddenArgsExtra(Extra):
|
|||
class FiltersExtra(Extra):
|
||||
description = "Filters object used by the HTML table interface"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
# Returns a Filters instance for the HTML templates - not JSON serializable
|
||||
public = False
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.filters
|
||||
|
|
@ -1034,8 +1040,10 @@ TABLE_EXTRA_CLASSES = [
|
|||
table_extra_registry = ExtraRegistry(TABLE_EXTRA_CLASSES)
|
||||
|
||||
|
||||
async def resolve_table_extras(extras, context):
|
||||
return await table_extra_registry.resolve(extras, context, ExtraScope.TABLE)
|
||||
async def resolve_table_extras(extras, context, include_internal=False):
|
||||
return await table_extra_registry.resolve(
|
||||
extras, context, ExtraScope.TABLE, include_internal=include_internal
|
||||
)
|
||||
|
||||
|
||||
async def resolve_row_extras(extras, context):
|
||||
|
|
|
|||
|
|
@ -425,9 +425,6 @@ The available table extras are listed below.
|
|||
}
|
||||
]
|
||||
|
||||
``display_rows``
|
||||
Row data formatted for the HTML table display
|
||||
|
||||
``render_cell``
|
||||
Rendered HTML for each cell using the render_cell plugin hook
|
||||
|
||||
|
|
@ -554,12 +551,6 @@ The available table extras are listed below.
|
|||
|
||||
"9403e5"
|
||||
|
||||
``actions``
|
||||
Table or view actions made available by plugin hooks
|
||||
|
||||
``filters``
|
||||
Filters object used by the HTML table interface
|
||||
|
||||
``renderers``
|
||||
Alternative output renderers available for this table
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,24 @@ async def test_query_extras_for_stored_query(ds_client):
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extra", ["filters", "actions", "display_rows"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_html_only_extras_are_not_available_via_json(ds_client, extra):
|
||||
# These extras exist for the HTML view; their values are not JSON
|
||||
# serializable so they are internal, not part of the JSON API
|
||||
response = await ds_client.get(f"/fixtures/facetable.json?_extra={extra}")
|
||||
assert response.status_code == 200
|
||||
assert extra not in response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_html_only_extras_are_not_advertised(ds_client):
|
||||
response = await ds_client.get("/fixtures/facetable.json?_extra=extras")
|
||||
assert response.status_code == 200
|
||||
names = {e["name"] for e in response.json()["extras"]}
|
||||
assert {"filters", "actions", "display_rows"}.isdisjoint(names)
|
||||
|
||||
|
||||
def test_query_extra_private_for_arbitrary_sql():
|
||||
with make_app_client(config={"allow_sql": {"id": "root"}}) as client:
|
||||
cookies = {"ds_actor": client.actor_cookie({"id": "root"})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue