Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions src/vfbquery/api_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,30 @@
#
# Parameter fields: name, doc, required (default False), example (pre-filled
# in the form), enum (renders a dropdown; "dynamic:<key>" pulls the list
# injected by build_docs_spec).
# injected by build_docs_spec), header (renders a checkbox that sends the
# named request header instead of a query parameter — see FORCE_REFRESH_PARAM
# below for why force_refresh needs this).
# ---------------------------------------------------------------------------

#: Shared definition for every endpoint that honours a cache refresh signal.
#: Rendered as a checkbox (see paramRow's "header" branch in DOCS_HTML) that
#: sets the ``X-Force-Refresh`` request header rather than a ``force_refresh``
#: query parameter. That distinction matters: appending ``&force_refresh=true``
#: to the URL changes nginx's cache key, so the refreshed response is written
#: into a *different* edge-cache slot and the canonical URL people actually
#: call is never healed. The header leaves the URL untouched, so one ticked
#: "Run" refreshes the v3-cached edge slot and the VFBquery-level cache
#: together, at the URL everyone else hits next — see ha_api._FORCE_REFRESH_HEADER
#: and CACHING.md for the server-side half of this.
FORCE_REFRESH_PARAM = {
"name": "force_refresh",
"header": "X-Force-Refresh",
"doc": ("Force a cache clear: bypasses the edge cache and makes VFBquery "
"recompute now. Refreshes the canonical URL directly, so this "
"call and the very next one — even unticked — both return fresh "
"data."),
}

ENDPOINT_GROUPS = [
{
"group": "Term information",
Expand All @@ -42,9 +63,7 @@
{"name": "id", "required": True,
"doc": "Ontology class or individual short_form",
"example": "FBbt_00003748"},
{"name": "force_refresh",
"doc": "true bypasses the result cache",
"example": ""},
FORCE_REFRESH_PARAM,
],
},
{
Expand All @@ -69,8 +88,7 @@
{"name": "include_graph",
"doc": "true adds a graph rendering of the rows",
"example": ""},
{"name": "force_refresh",
"doc": "true bypasses the result cache", "example": ""},
FORCE_REFRESH_PARAM,
],
},
{
Expand Down Expand Up @@ -203,6 +221,7 @@
"example": ""},
{"name": "include_graph",
"doc": "true adds a graph rendering", "example": ""},
FORCE_REFRESH_PARAM,
],
},
],
Expand Down Expand Up @@ -512,7 +531,13 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
function paramRow(param, kind){
const vocab = SPEC.vocabularies || {};
let input;
if (param.enum && String(param.enum).startsWith("dynamic:")){
if (param.header){
// A checkbox that sends a request header instead of a query parameter —
// see FORCE_REFRESH_PARAM in the Python source for why force_refresh
// needs this rather than the plain text input below.
input = el("input", {type: "checkbox", "data-name": param.name,
"data-kind": "header", "data-header-name": param.header});
} else if (param.enum && String(param.enum).startsWith("dynamic:")){
const key = param.enum.split(":")[1];
let values = [];
if (key === "query_types") values = vocab.query_types || [];
Expand All @@ -537,7 +562,14 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
function buildUrl(root, endpoint){
let path = endpoint.path;
const query = new URLSearchParams();
const headers = {};
for (const input of root.querySelectorAll("[data-name]")){
if (input.dataset.kind === "header"){
// Ticked -> the named header, at the unmodified URL below, never a
// query parameter — that's the whole point (see FORCE_REFRESH_PARAM).
if (input.checked) headers[input.dataset.headerName] = "true";
continue;
}
const value = input.value.trim();
if (input.dataset.kind === "path"){
if (!value) return {error: input.dataset.name + " is required"};
Expand All @@ -552,7 +584,7 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
}
}
const qs = query.toString();
return {url: path + (qs ? "?" + qs : "")};
return {url: path + (qs ? "?" + qs : ""), headers: headers};
}

async function run(root, endpoint){
Expand All @@ -561,13 +593,22 @@ def build_docs_spec(version, query_types=None, catmaid_commands=None):
const out = root.querySelector("pre.result");
const built = buildUrl(root, endpoint);
if (built.error){ meta.innerHTML = '<span class="bad">' + built.error + "</span>"; return; }
urlBox.textContent = new URL(built.url, window.location.href).href;
// A bypass checkbox is a one-shot action, not a sticky setting: reset it to
// off as soon as this run has captured whether it was ticked, so a bypass
// never carries over into someone re-running the same card a second time
// (e.g. to try a different id) without deliberately ticking it again.
for (const box of root.querySelectorAll('[data-kind="header"]')) box.checked = false;
const headerNames = Object.keys(built.headers || {});
urlBox.textContent = new URL(built.url, window.location.href).href +
(headerNames.length ? " (+ header " + headerNames.map(
(h) => h + ": " + built.headers[h]).join(", ") + ")" : "");
meta.textContent = "running…";
out.hidden = false;
out.textContent = "";
const started = performance.now();
try {
const resp = await fetch(built.url, {headers: {Accept: "application/json"}});
const resp = await fetch(built.url, {headers: Object.assign(
{Accept: "application/json"}, built.headers || {})});
const ms = Math.round(performance.now() - started);
const text = await resp.text();
let shown = text;
Expand Down
Loading