Manta Developers
GitHub
Menu

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:

  • binding says where the rows come from. select is a SPARQL SELECT with typed columns, lens reads one class through the fields you list, and query points at a saved query in the library.
  • render says how the rows are drawn. tsx is a written view, a React component in source form, and component points 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.

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: baseRevision is 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.
  • 401 or 403: the viewer is signed out, or may not use this datastore.
  • 502: the knowledge graph could not be reached.