mirror of
https://github.com/simonw/datasette.git
synced 2026-06-12 12:06:57 +02:00
Add row and query JSON extras
This commit is contained in:
parent
0fa872d438
commit
4d6daa175a
9 changed files with 861 additions and 129 deletions
|
|
@ -16,6 +16,8 @@ def extra_names_from_request(request):
|
|||
|
||||
class ExtraScope(Enum):
|
||||
TABLE = "table"
|
||||
ROW = "row"
|
||||
QUERY = "query"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -46,11 +48,16 @@ class Provider:
|
|||
class Extra(Provider):
|
||||
description: ClassVar[str | None] = None
|
||||
example: ClassVar[ExtraExample | None] = None
|
||||
examples: ClassVar[dict[ExtraScope, ExtraExample | list[ExtraExample]]] = {}
|
||||
public: ClassVar[bool] = True
|
||||
stable: ClassVar[bool] = True
|
||||
expensive: ClassVar[bool] = False
|
||||
docs_note: ClassVar[str | None] = None
|
||||
|
||||
@classmethod
|
||||
def example_for_scope(cls, scope):
|
||||
return cls.examples.get(scope, cls.example)
|
||||
|
||||
@classmethod
|
||||
def documentation(cls):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import sqlite_utils
|
|||
import textwrap
|
||||
|
||||
from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent
|
||||
from datasette.extras import extra_names_from_request
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.resources import DatabaseResource, QueryResource
|
||||
from datasette.stored_queries import stored_query_to_dict
|
||||
|
|
@ -38,6 +39,11 @@ from datasette.plugins import pm
|
|||
|
||||
from .base import BaseView, DatasetteError, View, _error, stream_csv
|
||||
from .query_helpers import _ensure_stored_query_execution_permissions, _table_columns
|
||||
from .table_extras import (
|
||||
QueryExtraContext,
|
||||
resolve_query_extras,
|
||||
table_extra_registry,
|
||||
)
|
||||
from . import Context
|
||||
|
||||
|
||||
|
|
@ -692,6 +698,34 @@ class QueryView(View):
|
|||
except DatasetteError:
|
||||
raise
|
||||
|
||||
extras = extra_names_from_request(request)
|
||||
metadata = None
|
||||
data = {"ok": True, "rows": rows, "columns": columns}
|
||||
if extras:
|
||||
metadata = await datasette.get_database_metadata(database)
|
||||
if stored_query:
|
||||
metadata = stored_query_to_dict(stored_query)
|
||||
metadata.pop("source", None)
|
||||
query_extra_context = QueryExtraContext(
|
||||
datasette=datasette,
|
||||
request=request,
|
||||
db=db,
|
||||
database_name=database,
|
||||
private=private,
|
||||
rows=rows,
|
||||
columns=columns,
|
||||
sql=sql,
|
||||
params=params_for_query,
|
||||
query_name=stored_query.name if stored_query else None,
|
||||
stored_query=stored_query,
|
||||
stored_query_write=stored_query_write,
|
||||
error=query_error,
|
||||
metadata=metadata,
|
||||
extras=extras,
|
||||
extra_registry=table_extra_registry,
|
||||
)
|
||||
data.update(await resolve_query_extras(extras, query_extra_context))
|
||||
|
||||
# Handle formats from plugins
|
||||
if format_ == "csv":
|
||||
if not sql:
|
||||
|
|
@ -721,7 +755,7 @@ class QueryView(View):
|
|||
error=query_error,
|
||||
# These will be deprecated in Datasette 1.0:
|
||||
args=request.args,
|
||||
data={"ok": True, "rows": rows, "columns": columns},
|
||||
data=data,
|
||||
)
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
|
@ -770,11 +804,11 @@ class QueryView(View):
|
|||
)
|
||||
}
|
||||
)
|
||||
metadata = await datasette.get_database_metadata(database)
|
||||
if stored_query:
|
||||
metadata = stored_query_to_dict(stored_query)
|
||||
metadata.pop("source", None)
|
||||
|
||||
if metadata is None:
|
||||
metadata = await datasette.get_database_metadata(database)
|
||||
if stored_query:
|
||||
metadata = stored_query_to_dict(stored_query)
|
||||
metadata.pop("source", None)
|
||||
renderers = {}
|
||||
for key, (_, can_render) in datasette.renderers.items():
|
||||
it_can_render = call_with_supported_arguments(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import json
|
|||
import markupsafe
|
||||
import sqlite_utils
|
||||
from .table import display_columns_and_rows, _get_extras
|
||||
from .table_extras import RowExtraContext, resolve_row_extras, table_extra_registry
|
||||
|
||||
|
||||
class RowView(DataView):
|
||||
|
|
@ -172,52 +173,26 @@ class RowView(DataView):
|
|||
extras.add("foreign_key_tables")
|
||||
|
||||
# Process extras
|
||||
if "foreign_key_tables" in extras:
|
||||
data["foreign_key_tables"] = await self.foreign_key_tables(
|
||||
database, table, pk_values
|
||||
)
|
||||
|
||||
if "render_cell" in extras:
|
||||
# Call render_cell plugin hook for each cell
|
||||
ct_map = await self.ds.get_column_types(database, table)
|
||||
rendered_rows = []
|
||||
for row in rows:
|
||||
rendered_row = {}
|
||||
for value, column in zip(row, columns):
|
||||
ct = ct_map.get(column)
|
||||
plugin_display_value = None
|
||||
# Try column type render_cell first
|
||||
if ct:
|
||||
candidate = await ct.render_cell(
|
||||
value=value,
|
||||
column=column,
|
||||
table=table,
|
||||
database=database,
|
||||
datasette=self.ds,
|
||||
request=request,
|
||||
)
|
||||
if candidate is not None:
|
||||
plugin_display_value = candidate
|
||||
if plugin_display_value is None:
|
||||
for candidate in pm.hook.render_cell(
|
||||
row=row,
|
||||
value=value,
|
||||
column=column,
|
||||
table=table,
|
||||
pks=resolved.pks,
|
||||
database=database,
|
||||
datasette=self.ds,
|
||||
request=request,
|
||||
column_type=ct,
|
||||
):
|
||||
candidate = await await_me_maybe(candidate)
|
||||
if candidate is not None:
|
||||
plugin_display_value = candidate
|
||||
break
|
||||
if plugin_display_value:
|
||||
rendered_row[column] = str(plugin_display_value)
|
||||
rendered_rows.append(rendered_row)
|
||||
data["render_cell"] = rendered_rows
|
||||
row_extra_context = RowExtraContext(
|
||||
datasette=self.ds,
|
||||
request=request,
|
||||
resolved=resolved,
|
||||
db=db,
|
||||
database_name=database,
|
||||
table_name=table,
|
||||
private=private,
|
||||
rows=rows,
|
||||
columns=columns,
|
||||
results_description=results.description,
|
||||
pks=pks,
|
||||
pk_values=pk_values,
|
||||
sql=resolved.sql,
|
||||
params=resolved.params,
|
||||
extras=extras,
|
||||
extra_registry=table_extra_registry,
|
||||
foreign_key_tables=self.foreign_key_tables,
|
||||
)
|
||||
data.update(await resolve_row_extras(extras, row_extra_context))
|
||||
|
||||
return (
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from datasette.resources import TableResource
|
|||
from datasette.utils import (
|
||||
await_me_maybe,
|
||||
call_with_supported_arguments,
|
||||
named_parameters as derive_named_parameters,
|
||||
path_with_added_args,
|
||||
path_with_format,
|
||||
path_with_removed_args,
|
||||
|
|
@ -52,6 +53,50 @@ class TableExtraContext:
|
|||
extra_registry: ExtraRegistry
|
||||
display_columns_and_rows: object
|
||||
run_sequential: object
|
||||
scope: ExtraScope = ExtraScope.TABLE
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RowExtraContext:
|
||||
datasette: object
|
||||
request: object
|
||||
resolved: object
|
||||
db: object
|
||||
database_name: str
|
||||
table_name: str
|
||||
private: bool
|
||||
rows: list
|
||||
columns: list
|
||||
results_description: list
|
||||
pks: list
|
||||
pk_values: list
|
||||
sql: str
|
||||
params: dict
|
||||
extras: set
|
||||
extra_registry: ExtraRegistry
|
||||
foreign_key_tables: object
|
||||
scope: ExtraScope = ExtraScope.ROW
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QueryExtraContext:
|
||||
datasette: object
|
||||
request: object
|
||||
db: object
|
||||
database_name: str
|
||||
private: bool
|
||||
rows: list
|
||||
columns: list
|
||||
sql: str | None
|
||||
params: dict
|
||||
query_name: str | None
|
||||
stored_query: object
|
||||
stored_query_write: bool
|
||||
error: str | None
|
||||
metadata: dict
|
||||
extras: set
|
||||
extra_registry: ExtraRegistry
|
||||
scope: ExtraScope = ExtraScope.QUERY
|
||||
|
||||
|
||||
class CountSqlExtra(Extra):
|
||||
|
|
@ -245,7 +290,15 @@ class NextUrlExtra(Extra):
|
|||
class ColumnsExtra(Extra):
|
||||
description = "Column names returned by this query"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=columns")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=columns"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=columns"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.columns
|
||||
|
|
@ -263,7 +316,12 @@ class AllColumnsExtra(Extra):
|
|||
class PrimaryKeysExtra(Extra):
|
||||
description = "Primary keys for this table"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=primary_keys")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=primary_keys"
|
||||
)
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.pks
|
||||
|
|
@ -309,21 +367,49 @@ class IsViewExtra(Extra):
|
|||
class DebugExtra(Extra):
|
||||
description = "Extra debug information"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=debug")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=debug"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=debug"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return {
|
||||
"resolved": repr(context.resolved),
|
||||
debug = {
|
||||
"url_vars": context.request.url_vars,
|
||||
"nofacet": context.nofacet,
|
||||
"nosuggest": context.nosuggest,
|
||||
}
|
||||
if context.scope == ExtraScope.TABLE:
|
||||
debug["resolved"] = repr(context.resolved)
|
||||
elif context.scope == ExtraScope.ROW:
|
||||
debug["resolved"] = {
|
||||
"table": context.table_name,
|
||||
"sql": context.sql,
|
||||
"params": context.params,
|
||||
"pks": context.pks,
|
||||
"pk_values": context.pk_values,
|
||||
}
|
||||
if hasattr(context, "nofacet"):
|
||||
debug["nofacet"] = context.nofacet
|
||||
if hasattr(context, "nosuggest"):
|
||||
debug["nosuggest"] = context.nosuggest
|
||||
return debug
|
||||
|
||||
|
||||
class RequestExtra(Extra):
|
||||
description = "Full information about the request"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=request")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=request"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=request"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return {
|
||||
|
|
@ -413,15 +499,48 @@ class RenderCellExtra(Extra):
|
|||
"whose rendered value differs from the default are included."
|
||||
),
|
||||
)
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
value={
|
||||
"rows": [{"id": 4, "content": "RENDER_CELL_DEMO"}],
|
||||
"render_cell": [{"content": "<strong>Custom rendered HTML</strong>"}],
|
||||
},
|
||||
note=(
|
||||
"The ``render_cell`` array has one item for the requested row. "
|
||||
"The object is keyed by column name. Only columns whose rendered "
|
||||
"value differs from the default are included."
|
||||
),
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
value={
|
||||
"rows": [{"content": "RENDER_CELL_DEMO"}],
|
||||
"render_cell": [{"content": "<strong>Custom rendered HTML</strong>"}],
|
||||
},
|
||||
note=(
|
||||
"The ``render_cell`` array has one item per query result row, in "
|
||||
"the same order as the ``rows`` array. Each object is keyed by "
|
||||
"column name. Only columns whose rendered value differs from the "
|
||||
"default are included."
|
||||
),
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
table_name = getattr(context, "table_name", None)
|
||||
is_view = getattr(context, "is_view", False)
|
||||
pks = getattr(context, "pks", [])
|
||||
pks_for_display = (
|
||||
context.pks if context.pks else (["rowid"] if not context.is_view else [])
|
||||
pks if pks else (["rowid"] if table_name and not is_view else [])
|
||||
)
|
||||
col_names = [col[0] for col in context.results_description]
|
||||
ct_map = await context.datasette.get_column_types(
|
||||
context.database_name, context.table_name
|
||||
if hasattr(context, "results_description"):
|
||||
col_names = [col[0] for col in context.results_description]
|
||||
else:
|
||||
col_names = context.columns
|
||||
ct_map = (
|
||||
await context.datasette.get_column_types(context.database_name, table_name)
|
||||
if table_name
|
||||
else {}
|
||||
)
|
||||
rendered_rows = []
|
||||
for row in context.rows:
|
||||
|
|
@ -433,7 +552,7 @@ class RenderCellExtra(Extra):
|
|||
candidate = await ct.render_cell(
|
||||
value=value,
|
||||
column=column,
|
||||
table=context.table_name,
|
||||
table=table_name,
|
||||
database=context.database_name,
|
||||
datasette=context.datasette,
|
||||
request=context.request,
|
||||
|
|
@ -445,7 +564,7 @@ class RenderCellExtra(Extra):
|
|||
row=row,
|
||||
value=value,
|
||||
column=column,
|
||||
table=context.table_name,
|
||||
table=table_name,
|
||||
pks=pks_for_display,
|
||||
database=context.database_name,
|
||||
datasette=context.datasette,
|
||||
|
|
@ -465,19 +584,36 @@ class RenderCellExtra(Extra):
|
|||
class QueryExtra(Extra):
|
||||
description = "Details of the underlying SQL query"
|
||||
example = ExtraExample("/fixtures/facetable.json?_size=1&_extra=query")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=query"
|
||||
),
|
||||
ExtraScope.QUERY: [
|
||||
ExtraExample("/fixtures/-/query.json?sql=select+1+as+one&_extra=query"),
|
||||
ExtraExample("/fixtures/neighborhood_search.json?text=town&_extra=query"),
|
||||
],
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
params = context.params
|
||||
if context.scope == ExtraScope.QUERY and context.sql:
|
||||
parameter_names = set(derive_named_parameters(context.sql))
|
||||
params = {
|
||||
key: value
|
||||
for key, value in dict(context.params).items()
|
||||
if key in parameter_names
|
||||
}
|
||||
return {
|
||||
"sql": context.sql,
|
||||
"params": context.params,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
|
||||
class ColumnTypesExtra(Extra):
|
||||
description = "Column type assignments for this table"
|
||||
example = ExtraExample(value={})
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW})
|
||||
|
||||
async def resolve(self, context):
|
||||
ct_map = await context.datasette.get_column_types(
|
||||
|
|
@ -544,11 +680,22 @@ class SetColumnTypeUiExtra(Extra):
|
|||
|
||||
|
||||
class MetadataExtra(Extra):
|
||||
description = "Metadata about the table and database"
|
||||
description = "Metadata about the table, database or stored query"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=metadata")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=metadata"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/neighborhood_search.json?text=town&_extra=metadata"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
if context.scope == ExtraScope.QUERY:
|
||||
return context.metadata
|
||||
|
||||
tablemetadata = await context.datasette.get_resource_metadata(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
|
|
@ -572,7 +719,15 @@ class MetadataExtra(Extra):
|
|||
class DatabaseExtra(Extra):
|
||||
description = "Database name"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=database")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=database"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=database"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.database_name
|
||||
|
|
@ -581,7 +736,10 @@ class DatabaseExtra(Extra):
|
|||
class TableExtra(Extra):
|
||||
description = "Table name"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=table")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample("/fixtures/simple_primary_key/1.json?_extra=table")
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.table_name
|
||||
|
|
@ -590,7 +748,15 @@ class TableExtra(Extra):
|
|||
class DatabaseColorExtra(Extra):
|
||||
description = "Color assigned to the database"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=database_color")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=database_color"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=database_color"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.db.color
|
||||
|
|
@ -703,6 +869,8 @@ class RenderersExtra(Extra):
|
|||
url_labels_extra = {}
|
||||
if expandable_columns:
|
||||
url_labels_extra = {"_labels": "on"}
|
||||
table_name = getattr(context, "table_name", None)
|
||||
view_name = "table" if context.scope == ExtraScope.TABLE else "database"
|
||||
for key, (_, can_render) in context.datasette.renderers.items():
|
||||
it_can_render = call_with_supported_arguments(
|
||||
can_render,
|
||||
|
|
@ -710,11 +878,11 @@ class RenderersExtra(Extra):
|
|||
columns=context.columns or [],
|
||||
rows=context.rows or [],
|
||||
sql=query.get("sql", None),
|
||||
query_name=None,
|
||||
query_name=getattr(context, "query_name", None),
|
||||
database=context.database_name,
|
||||
table=context.table_name,
|
||||
table=table_name,
|
||||
request=context.request,
|
||||
view_name="table",
|
||||
view_name=view_name,
|
||||
)
|
||||
it_can_render = await await_me_maybe(it_can_render)
|
||||
if it_can_render:
|
||||
|
|
@ -730,9 +898,17 @@ class RenderersExtra(Extra):
|
|||
|
||||
|
||||
class PrivateExtra(Extra):
|
||||
description = "Whether this table is private to the current actor"
|
||||
description = "Whether this resource is private to the current actor"
|
||||
example = ExtraExample("/fixtures/facetable.json?_extra=private")
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
examples = {
|
||||
ExtraScope.ROW: ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=private"
|
||||
),
|
||||
ExtraScope.QUERY: ExtraExample(
|
||||
"/fixtures/-/query.json?sql=select+1+as+one&_extra=private"
|
||||
),
|
||||
}
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.private
|
||||
|
|
@ -752,14 +928,27 @@ class ExpandableColumnsExtra(Extra):
|
|||
return expandables
|
||||
|
||||
|
||||
class ForeignKeyTablesExtra(Extra):
|
||||
description = "Tables that link to this row using foreign keys"
|
||||
example = ExtraExample(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=foreign_key_tables"
|
||||
)
|
||||
scopes = frozenset({ExtraScope.ROW})
|
||||
|
||||
async def resolve(self, context):
|
||||
return await context.foreign_key_tables(
|
||||
context.database_name, context.table_name, context.pk_values
|
||||
)
|
||||
|
||||
|
||||
class ExtrasExtra(Extra):
|
||||
description = "Available ?_extra= blocks"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
scopes = frozenset({ExtraScope.TABLE, ExtraScope.ROW, ExtraScope.QUERY})
|
||||
|
||||
async def resolve(self, context):
|
||||
all_extras = [
|
||||
(cls.key(), cls.description)
|
||||
for cls in context.extra_registry.public_classes_for_scope(ExtraScope.TABLE)
|
||||
for cls in context.extra_registry.public_classes_for_scope(context.scope)
|
||||
]
|
||||
return [
|
||||
{
|
||||
|
|
@ -850,6 +1039,7 @@ TABLE_EXTRA_CLASSES = [
|
|||
IsViewExtra,
|
||||
PrivateExtra,
|
||||
ExpandableColumnsExtra,
|
||||
ForeignKeyTablesExtra,
|
||||
FormHiddenArgsExtra,
|
||||
]
|
||||
|
||||
|
|
@ -859,3 +1049,11 @@ 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_row_extras(extras, context):
|
||||
return await table_extra_registry.resolve(extras, context, ExtraScope.ROW)
|
||||
|
||||
|
||||
async def resolve_query_extras(extras, context):
|
||||
return await table_extra_registry.resolve(extras, context, ExtraScope.QUERY)
|
||||
|
|
|
|||
|
|
@ -237,23 +237,26 @@ query string arguments:
|
|||
|
||||
.. _json_api_extra:
|
||||
|
||||
Expanding table JSON responses
|
||||
------------------------------
|
||||
Expanding JSON responses
|
||||
------------------------
|
||||
|
||||
Table JSON responses can be expanded with one or more ``?_extra=`` parameters.
|
||||
Table, row and query JSON responses can be expanded with one or more ``?_extra=`` parameters.
|
||||
These can be repeated or comma-separated:
|
||||
|
||||
::
|
||||
|
||||
?_extra=columns&_extra=count,next_url
|
||||
|
||||
The available table extras are listed below.
|
||||
|
||||
.. [[[cog
|
||||
from json_api_doc import table_extras
|
||||
table_extras(cog)
|
||||
.. ]]]
|
||||
|
||||
Table JSON responses
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The available table extras are listed below.
|
||||
|
||||
``count``
|
||||
Total count of rows matching these filters (May execute additional queries.)
|
||||
|
||||
|
|
@ -459,12 +462,12 @@ The available table extras are listed below.
|
|||
.. code-block:: json
|
||||
|
||||
{
|
||||
"resolved": "ResolvedTable(db=<Database: fixtures (mutable, size=249856)>, table='facetable', is_view=False)",
|
||||
"url_vars": {
|
||||
"database": "fixtures",
|
||||
"table": "facetable",
|
||||
"format": "json"
|
||||
},
|
||||
"resolved": "ResolvedTable(db=<Database: fixtures (mutable, size=249856)>, table='facetable', is_view=False)",
|
||||
"nofacet": null,
|
||||
"nosuggest": null
|
||||
}
|
||||
|
|
@ -511,7 +514,7 @@ The available table extras are listed below.
|
|||
Column type UI metadata for this table
|
||||
|
||||
``metadata``
|
||||
Metadata about the table and database
|
||||
Metadata about the table, database or stored query
|
||||
|
||||
``GET /fixtures/facetable.json?_extra=metadata``
|
||||
|
||||
|
|
@ -649,7 +652,7 @@ The available table extras are listed below.
|
|||
true
|
||||
|
||||
``private``
|
||||
Whether this table is private to the current actor
|
||||
Whether this resource is private to the current actor
|
||||
|
||||
``GET /fixtures/facetable.json?_extra=private``
|
||||
|
||||
|
|
@ -697,6 +700,373 @@ The available table extras are listed below.
|
|||
]
|
||||
]
|
||||
|
||||
Row JSON responses
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following extras are available for row JSON responses.
|
||||
|
||||
``columns``
|
||||
Column names returned by this query
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=columns``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
"id",
|
||||
"content"
|
||||
]
|
||||
|
||||
``primary_keys``
|
||||
Primary keys for this table
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=primary_keys``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
"id"
|
||||
]
|
||||
|
||||
``render_cell``
|
||||
Rendered HTML for each cell using the render_cell plugin hook
|
||||
|
||||
The ``render_cell`` array has one item for the requested row. The object is keyed by column name. Only columns whose rendered value differs from the default are included.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"id": 4,
|
||||
"content": "RENDER_CELL_DEMO"
|
||||
}
|
||||
],
|
||||
"render_cell": [
|
||||
{
|
||||
"content": "<strong>Custom rendered HTML</strong>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
``debug``
|
||||
Extra debug information
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=debug``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"url_vars": {
|
||||
"database": "fixtures",
|
||||
"table": "simple_primary_key",
|
||||
"pks": "1",
|
||||
"format": "json"
|
||||
},
|
||||
"resolved": {
|
||||
"table": "simple_primary_key",
|
||||
"sql": "select * from simple_primary_key where \"id\"=:p0",
|
||||
"params": {
|
||||
"p0": "1"
|
||||
},
|
||||
"pks": [
|
||||
"id"
|
||||
],
|
||||
"pk_values": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
``request``
|
||||
Full information about the request
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=request``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"url": "http://localhost/fixtures/simple_primary_key/1.json?_extra=request",
|
||||
"path": "/fixtures/simple_primary_key/1.json",
|
||||
"full_path": "/fixtures/simple_primary_key/1.json?_extra=request",
|
||||
"host": "localhost",
|
||||
"args": {
|
||||
"_extra": [
|
||||
"request"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
``query``
|
||||
Details of the underlying SQL query
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=query``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"sql": "select * from simple_primary_key where \"id\"=:p0",
|
||||
"params": {
|
||||
"p0": "1"
|
||||
}
|
||||
}
|
||||
|
||||
``column_types``
|
||||
Column type assignments for this table
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{}
|
||||
|
||||
``metadata``
|
||||
Metadata about the table, database or stored query
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=metadata``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"columns": {}
|
||||
}
|
||||
|
||||
``extras``
|
||||
Available ?_extra= blocks
|
||||
|
||||
``database``
|
||||
Database name
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=database``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"fixtures"
|
||||
|
||||
``table``
|
||||
Table name
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=table``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"simple_primary_key"
|
||||
|
||||
``database_color``
|
||||
Color assigned to the database
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=database_color``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"9403e5"
|
||||
|
||||
``private``
|
||||
Whether this resource is private to the current actor
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=private``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
false
|
||||
|
||||
``foreign_key_tables``
|
||||
Tables that link to this row using foreign keys
|
||||
|
||||
``GET /fixtures/simple_primary_key/1.json?_extra=foreign_key_tables``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"other_table": "complex_foreign_keys",
|
||||
"column": "id",
|
||||
"other_column": "f1",
|
||||
"count": 1,
|
||||
"link": "/fixtures/complex_foreign_keys?f1=1"
|
||||
},
|
||||
{
|
||||
"other_table": "complex_foreign_keys",
|
||||
"column": "id",
|
||||
"other_column": "f2",
|
||||
"count": 0,
|
||||
"link": "/fixtures/complex_foreign_keys?f2=1"
|
||||
},
|
||||
{
|
||||
"other_table": "complex_foreign_keys",
|
||||
"column": "id",
|
||||
"other_column": "f3",
|
||||
"count": 1,
|
||||
"link": "/fixtures/complex_foreign_keys?f3=1"
|
||||
},
|
||||
{
|
||||
"other_table": "foreign_key_references",
|
||||
"column": "id",
|
||||
"other_column": "foreign_key_with_blank_label",
|
||||
"count": 0,
|
||||
"link": "/fixtures/foreign_key_references?foreign_key_with_blank_label=1"
|
||||
},
|
||||
{
|
||||
"other_table": "foreign_key_references",
|
||||
"column": "id",
|
||||
"other_column": "foreign_key_with_label",
|
||||
"count": 1,
|
||||
"link": "/fixtures/foreign_key_references?foreign_key_with_label=1"
|
||||
}
|
||||
]
|
||||
|
||||
Query JSON responses
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following extras are available for arbitrary SQL query responses and stored, named query responses.
|
||||
|
||||
``columns``
|
||||
Column names returned by this query
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=columns``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
"one"
|
||||
]
|
||||
|
||||
``render_cell``
|
||||
Rendered HTML for each cell using the render_cell plugin hook
|
||||
|
||||
The ``render_cell`` array has one item per query result row, in the same order as the ``rows`` array. Each object is keyed by column name. Only columns whose rendered value differs from the default are included.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"content": "RENDER_CELL_DEMO"
|
||||
}
|
||||
],
|
||||
"render_cell": [
|
||||
{
|
||||
"content": "<strong>Custom rendered HTML</strong>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
``debug``
|
||||
Extra debug information
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=debug``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"url_vars": {
|
||||
"database": "fixtures",
|
||||
"format": "json"
|
||||
}
|
||||
}
|
||||
|
||||
``request``
|
||||
Full information about the request
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=request``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"url": "http://localhost/fixtures/-/query.json?sql=select+1+as+one&_extra=request",
|
||||
"path": "/fixtures/-/query.json",
|
||||
"full_path": "/fixtures/-/query.json?sql=select+1+as+one&_extra=request",
|
||||
"host": "localhost",
|
||||
"args": {
|
||||
"sql": [
|
||||
"select 1 as one"
|
||||
],
|
||||
"_extra": [
|
||||
"request"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
``query``
|
||||
Details of the underlying SQL query
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=query``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"sql": "select 1 as one",
|
||||
"params": {}
|
||||
}
|
||||
|
||||
``GET /fixtures/neighborhood_search.json?text=town&_extra=query``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"sql": "\nselect _neighborhood, facet_cities.name, state\nfrom facetable\n join facet_cities\n on facetable._city_id = facet_cities.id\nwhere _neighborhood like '%' || :text || '%'\norder by _neighborhood;\n",
|
||||
"params": {
|
||||
"text": "town"
|
||||
}
|
||||
}
|
||||
|
||||
``metadata``
|
||||
Metadata about the table, database or stored query
|
||||
|
||||
``GET /fixtures/neighborhood_search.json?text=town&_extra=metadata``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"database": "fixtures",
|
||||
"name": "neighborhood_search",
|
||||
"sql": "\nselect _neighborhood, facet_cities.name, state\nfrom facetable\n join facet_cities\n on facetable._city_id = facet_cities.id\nwhere _neighborhood like '%' || :text || '%'\norder by _neighborhood;\n",
|
||||
"title": "Search neighborhoods",
|
||||
"description": null,
|
||||
"description_html": null,
|
||||
"hide_sql": false,
|
||||
"fragment": null,
|
||||
"params": [],
|
||||
"parameters": [],
|
||||
"is_write": false,
|
||||
"is_private": false,
|
||||
"is_trusted": true,
|
||||
"owner_id": null,
|
||||
"on_success_message": null,
|
||||
"on_success_message_sql": null,
|
||||
"on_success_redirect": null,
|
||||
"on_error_message": null,
|
||||
"on_error_redirect": null
|
||||
}
|
||||
|
||||
``extras``
|
||||
Available ?_extra= blocks
|
||||
|
||||
``database``
|
||||
Database name
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=database``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"fixtures"
|
||||
|
||||
``database_color``
|
||||
Color assigned to the database
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=database_color``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"9403e5"
|
||||
|
||||
``private``
|
||||
Whether this resource is private to the current actor
|
||||
|
||||
``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=private``
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
false
|
||||
|
||||
.. [[[end]]]
|
||||
|
||||
.. _table_arguments:
|
||||
|
|
|
|||
|
|
@ -9,39 +9,80 @@ def table_extras(cog):
|
|||
from datasette.extras import ExtraScope
|
||||
from datasette.views.table_extras import table_extra_registry
|
||||
|
||||
classes = table_extra_registry.public_classes_for_scope(ExtraScope.TABLE)
|
||||
scopes = [
|
||||
(
|
||||
ExtraScope.TABLE,
|
||||
"Table JSON responses",
|
||||
"The available table extras are listed below.",
|
||||
),
|
||||
(
|
||||
ExtraScope.ROW,
|
||||
"Row JSON responses",
|
||||
"The following extras are available for row JSON responses.",
|
||||
),
|
||||
(
|
||||
ExtraScope.QUERY,
|
||||
"Query JSON responses",
|
||||
(
|
||||
"The following extras are available for arbitrary SQL query "
|
||||
"responses and stored, named query responses."
|
||||
),
|
||||
),
|
||||
]
|
||||
classes_by_scope = [
|
||||
(scope, heading, intro, table_extra_registry.public_classes_for_scope(scope))
|
||||
for scope, heading, intro in scopes
|
||||
]
|
||||
|
||||
live_examples = asyncio.run(_fetch_live_examples(classes))
|
||||
live_examples = asyncio.run(
|
||||
_fetch_live_examples(
|
||||
[
|
||||
(scope, cls)
|
||||
for scope, _, _, classes in classes_by_scope
|
||||
for cls in classes
|
||||
]
|
||||
)
|
||||
)
|
||||
cog.out("\n")
|
||||
for cls in classes:
|
||||
example = cls.example
|
||||
description = cls.description or ""
|
||||
notes = []
|
||||
if cls.expensive:
|
||||
notes.append("May execute additional queries.")
|
||||
if cls.docs_note:
|
||||
notes.append(cls.docs_note)
|
||||
if notes:
|
||||
description = "{} ({})".format(description, " ".join(notes)).strip()
|
||||
for scope, heading, intro, classes in classes_by_scope:
|
||||
cog.out("{}\n{}\n\n".format(heading, "~" * len(heading)))
|
||||
cog.out("{}\n\n".format(intro))
|
||||
for cls in classes:
|
||||
examples = _examples_for_scope(cls, scope)
|
||||
description = cls.description or ""
|
||||
notes = []
|
||||
if cls.expensive:
|
||||
notes.append("May execute additional queries.")
|
||||
if cls.docs_note:
|
||||
notes.append(cls.docs_note)
|
||||
if notes:
|
||||
description = "{} ({})".format(description, " ".join(notes)).strip()
|
||||
|
||||
cog.out("``{}``\n".format(cls.key()))
|
||||
cog.out(" {}\n\n".format(description))
|
||||
if example is None:
|
||||
continue
|
||||
|
||||
if example.path:
|
||||
value = live_examples[(example.path, example.key or cls.key())]
|
||||
cog.out(" ``GET {}``\n\n".format(example.path))
|
||||
else:
|
||||
value = example.value
|
||||
if example.note:
|
||||
cog.out(" {}\n\n".format(example.note))
|
||||
cog.out(" .. code-block:: json\n\n")
|
||||
cog.out(textwrap.indent(json.dumps(value, indent=2), " "))
|
||||
cog.out("\n\n")
|
||||
cog.out("``{}``\n".format(cls.key()))
|
||||
cog.out(" {}\n\n".format(description))
|
||||
for example in examples:
|
||||
if example.path:
|
||||
value = live_examples[(example.path, example.key or cls.key())]
|
||||
cog.out(" ``GET {}``\n\n".format(example.path))
|
||||
else:
|
||||
value = example.value
|
||||
if example.note:
|
||||
cog.out(" {}\n\n".format(example.note))
|
||||
cog.out(" .. code-block:: json\n\n")
|
||||
cog.out(textwrap.indent(json.dumps(value, indent=2), " "))
|
||||
cog.out("\n\n")
|
||||
|
||||
|
||||
async def _fetch_live_examples(classes):
|
||||
def _examples_for_scope(cls, scope):
|
||||
examples = cls.example_for_scope(scope)
|
||||
if examples is None:
|
||||
return []
|
||||
if isinstance(examples, list):
|
||||
return examples
|
||||
return [examples]
|
||||
|
||||
|
||||
async def _fetch_live_examples(scoped_classes):
|
||||
from datasette.app import Datasette
|
||||
from datasette.fixtures import write_fixture_database
|
||||
|
||||
|
|
@ -49,18 +90,40 @@ async def _fetch_live_examples(classes):
|
|||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = pathlib.Path(tmpdir) / "fixtures.db"
|
||||
write_fixture_database(db_path)
|
||||
datasette = Datasette([str(db_path)], settings={"num_sql_threads": 1})
|
||||
datasette = Datasette(
|
||||
[str(db_path)],
|
||||
settings={"num_sql_threads": 1},
|
||||
config={
|
||||
"databases": {
|
||||
"fixtures": {
|
||||
"queries": {
|
||||
"neighborhood_search": {
|
||||
"sql": textwrap.dedent("""
|
||||
select _neighborhood, facet_cities.name, state
|
||||
from facetable
|
||||
join facet_cities
|
||||
on facetable._city_id = facet_cities.id
|
||||
where _neighborhood like '%' || :text || '%'
|
||||
order by _neighborhood;
|
||||
"""),
|
||||
"title": "Search neighborhoods",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
try:
|
||||
for cls in classes:
|
||||
example = cls.example
|
||||
if example is None or not example.path:
|
||||
continue
|
||||
key = example.key or cls.key()
|
||||
response = await datasette.client.get(example.path)
|
||||
assert response.status_code == 200, example.path
|
||||
data = response.json()
|
||||
assert key in data, "{} missing from {}".format(key, example.path)
|
||||
examples[(example.path, key)] = data[key]
|
||||
for scope, cls in scoped_classes:
|
||||
for example in _examples_for_scope(cls, scope):
|
||||
if not example.path:
|
||||
continue
|
||||
key = example.key or cls.key()
|
||||
response = await datasette.client.get(example.path)
|
||||
assert response.status_code == 200, example.path
|
||||
data = response.json()
|
||||
assert key in data, "{} missing from {}".format(key, example.path)
|
||||
examples[(example.path, key)] = data[key]
|
||||
finally:
|
||||
for db in datasette.databases.values():
|
||||
if not db.is_memory:
|
||||
|
|
|
|||
|
|
@ -426,6 +426,28 @@ async def test_row_foreign_key_tables(ds_client):
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_row_extras(ds_client):
|
||||
response = await ds_client.get(
|
||||
"/fixtures/simple_primary_key/1.json?_extra=database,table,primary_keys,query,request,debug,foreign_key_tables"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["database"] == "fixtures"
|
||||
assert data["table"] == "simple_primary_key"
|
||||
assert data["primary_keys"] == ["id"]
|
||||
assert data["query"]["sql"] == 'select * from simple_primary_key where "id"=:p0'
|
||||
assert data["query"]["params"] == {"p0": "1"}
|
||||
assert data["request"]["path"] == "/fixtures/simple_primary_key/1.json"
|
||||
assert data["debug"]["url_vars"] == {
|
||||
"database": "fixtures",
|
||||
"table": "simple_primary_key",
|
||||
"pks": "1",
|
||||
"format": "json",
|
||||
}
|
||||
assert len(data["foreign_key_tables"]) == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_row_extra_render_cell():
|
||||
"""Test that _extra=render_cell returns rendered HTML from render_cell plugin hook on row pages"""
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ def test_render_cell_extra_example_explains_row_and_column_mapping():
|
|||
|
||||
def test_debug_and_request_extra_examples_are_documented():
|
||||
content = (docs_path / "json_api.rst").read_text()
|
||||
section = content.split(".. _json_api_extra:")[-1].split(".. _table_arguments:")[0]
|
||||
section = content.split("Table JSON responses")[-1].split("Row JSON responses")[0]
|
||||
|
||||
debug_section = section.split("``debug``")[-1].split("``request``")[0]
|
||||
assert "GET /fixtures/facetable.json?_extra=debug" in debug_section
|
||||
|
|
@ -143,6 +143,20 @@ def test_debug_and_request_extra_examples_are_documented():
|
|||
assert '"full_path":' in request_section
|
||||
|
||||
|
||||
def test_row_and_query_extra_sections_are_documented():
|
||||
content = (docs_path / "json_api.rst").read_text()
|
||||
assert "Row JSON responses" in content
|
||||
assert (
|
||||
"``GET /fixtures/simple_primary_key/1.json?_extra=foreign_key_tables``"
|
||||
in content
|
||||
)
|
||||
assert "Query JSON responses" in content
|
||||
assert "``GET /fixtures/-/query.json?sql=select+1+as+one&_extra=query``" in content
|
||||
assert (
|
||||
"``GET /fixtures/neighborhood_search.json?text=town&_extra=query``" in content
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def documented_labels():
|
||||
labels = set()
|
||||
|
|
|
|||
|
|
@ -68,6 +68,55 @@ async def test_table_shape_arrayfirst(ds_client):
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_extras_for_arbitrary_sql(ds_client):
|
||||
response = await ds_client.get(
|
||||
"/fixtures/-/query.json?"
|
||||
+ urllib.parse.urlencode(
|
||||
{
|
||||
"sql": "select 1 as one",
|
||||
"_extra": "columns,database,query,request,debug",
|
||||
}
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["rows"] == [{"one": 1}]
|
||||
assert data["columns"] == ["one"]
|
||||
assert data["database"] == "fixtures"
|
||||
assert data["query"]["sql"] == "select 1 as one"
|
||||
assert data["request"]["path"] == "/fixtures/-/query.json"
|
||||
assert data["debug"]["url_vars"] == {
|
||||
"database": "fixtures",
|
||||
"format": "json",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_extras_for_stored_query(ds_client):
|
||||
response = await ds_client.get(
|
||||
"/fixtures/neighborhood_search.json?"
|
||||
+ urllib.parse.urlencode(
|
||||
{
|
||||
"text": "town",
|
||||
"_extra": "columns,database,query,request,debug",
|
||||
}
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["columns"] == ["_neighborhood", "name", "state"]
|
||||
assert data["database"] == "fixtures"
|
||||
assert data["query"]["sql"].strip().startswith("select _neighborhood")
|
||||
assert data["query"]["params"]["text"] == "town"
|
||||
assert data["request"]["path"] == "/fixtures/neighborhood_search.json"
|
||||
assert data["debug"]["url_vars"] == {
|
||||
"database": "fixtures",
|
||||
"table": "neighborhood_search",
|
||||
"format": "json",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_shape_objects(ds_client):
|
||||
response = await ds_client.get("/fixtures/simple_primary_key.json?_shape=objects")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue