Horn and HornView
The JSON documents a Horn is made of, and how to build and save one.
What it is
A Horn is one dashboard. It names its datastore and realm, carries a revision, and lays its views out on a
12-column grid. A HornView is one card on that grid, and pairs a binding with a render:
bindingsays where the rows come from.selectis a SPARQLSELECTwith typed columns,lensreads one class through the fields you list, andquerypoints at a saved query in the library.rendersays how the rows are drawn.tsxis a written view, a React component in source form, andcomponentpoints at a component in the library.
Both schemas are zod schemas, so you can validate a Horn before you send it anywhere. A binding returns at most 1,000 rows, and a written view’s source is at most 8,000 characters.
Import
import { hornSchema, hornViewSchema, parseRealm, type Horn, type HornView } from "@manta-eu/horns";
import { standaloneHorn } from "@manta-eu/horns/shapes";
Example
standaloneHorn wraps your views in a Horn that stacks them full width, so you only write the views:
const openTasks: HornView = {
id: "open-tasks",
title: "Open tasks",
binding: {
kind: "select",
sparql:
"SELECT ?task ?title WHERE { ?task a <https://example.org/vocab#Task> ; " +
"<https://example.org/vocab#title> ?title ; <https://example.org/vocab#completed> false }",
params: [],
limit: 50,
derivations: [],
columns: [
{ name: "task", type: "iri" },
{ name: "title", type: "string" },
],
},
render: {
kind: "tsx",
source: `export default function OpenTasks({ rows }) {
return <ul>{rows.map((row) => <li key={String(row.task)}>{String(row.title)}</li>)}</ul>;
}`,
},
};
export const horn: Horn = hornSchema.parse(
standaloneHorn({
id: "open-tasks",
title: "Open tasks",
views: [openTasks],
realm: parseRealm(realmId),
datastoreId: "your-datastore",
}),
);
realmId is the id of the realm your Manta account runs in, the same value the Horns server reads
as VITE_KEYCLOAK_REALM. parseRealm throws for a realm it does not know.
A written view is the module’s export default. It gets rows and columns, plus labels and links for every IRI
in the rows, so it can show a name instead of an address.
A Horn from standaloneHorn is for drawing, not saving: pass it to HornDrawn. A Horn people come
back to is saved with its own slug, and the server assigns its IRI and revision.
Request
Saving a Horn is a PUT to the Horns server on your app’s own origin:
PUT /api/d/your-datastore/horns/open-tasks
Content-Type: application/json
{ "horn": { "…": "the Horn" }, "baseRevision": "<the revision you opened>" }
Leave baseRevision out the first time you save a slug. After that, send the revision the last read or save returned.
The server refuses a save whose baseRevision is not the current one, so nobody overwrites someone else’s edit.
Response
{
"iri": "<the Horn's IRI>",
"slug": "open-tasks",
"revision": "<the new revision's IRI>",
"contentHash": "…"
}
The server owns the IRI and the revision. A document that names others is saved under the server’s.
A save is refused with { "error": "…" } when:
409:baseRevisionis not the current revision, or names a revision of a slug that was never saved.400: the Horn fails validation, its library lock does not verify, or a written view has not been reviewed.401or403: the viewer is signed out, or may not use this datastore.502: the knowledge graph could not be reached.