@manta-eu/horns-server 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/horn-routes.js +77 -0
- package/package.json +3 -3
package/dist/horn-routes.js
CHANGED
|
@@ -484,6 +484,83 @@ export function hornRoutes(deps = {}) {
|
|
|
484
484
|
* Two segments because one would not be safe: `about` is a legal slug, so `/horns/about` is a Horn
|
|
485
485
|
* somebody may have saved, and `/horns/:slug` would answer for it either way.
|
|
486
486
|
*/
|
|
487
|
+
// Three fit beside a thing without pushing it off the screen; ten is where the reading cost stops being
|
|
488
|
+
// one query and starts being a page of them.
|
|
489
|
+
const defaultAboutLimit = 3;
|
|
490
|
+
const maxAboutLimit = 10;
|
|
491
|
+
/**
|
|
492
|
+
* Every Horn about a thing, for a host that has room for more than one.
|
|
493
|
+
*
|
|
494
|
+
* The singular route answers with one and counts the rest, which is what a panel beside a plan wants.
|
|
495
|
+
* A host showing a thing's whole story wants them all: `?limit=` caps how many come back whole, and
|
|
496
|
+
* `total` says how many there are. Nothing kept falls back the same way the singular route does, so an
|
|
497
|
+
* IRI nobody has made a Horn for still draws.
|
|
498
|
+
*/
|
|
499
|
+
routes.get("/api/d/:datastoreId/horns/by/about/list", async (c) => {
|
|
500
|
+
const datastoreId = c.req.param("datastoreId");
|
|
501
|
+
const authorized = await authorize(c.req.raw, c.env, datastoreId);
|
|
502
|
+
if (!authorized.ok)
|
|
503
|
+
return authorized.response;
|
|
504
|
+
const realm = c.env.VITE_KEYCLOAK_REALM;
|
|
505
|
+
if (!isPresent(realm) || !isRealm(realm))
|
|
506
|
+
return refuse(500, "This deployment has no realm configured.").response;
|
|
507
|
+
const about = c.req.query("iri");
|
|
508
|
+
if (!isPresent(about))
|
|
509
|
+
return refuse(400, "Ask with ?iri=<the thing's IRI>.").response;
|
|
510
|
+
if (!isAbsoluteIri(about))
|
|
511
|
+
return refuse(400, `"${about}" is not an IRI.`).response;
|
|
512
|
+
const asked = Number(c.req.query("limit") ?? defaultAboutLimit);
|
|
513
|
+
if (!Number.isInteger(asked) || asked < 1 || asked > maxAboutLimit) {
|
|
514
|
+
return refuse(400, `limit is a whole number from 1 to ${maxAboutLimit}.`).response;
|
|
515
|
+
}
|
|
516
|
+
const select = agentsSparqlSelect({
|
|
517
|
+
agentsUrl: c.env.MANTA_AGENTS_URL,
|
|
518
|
+
upstream: c.env.UPSTREAM,
|
|
519
|
+
session: authorized.session,
|
|
520
|
+
datastoreId,
|
|
521
|
+
});
|
|
522
|
+
try {
|
|
523
|
+
const found = await select(hornsAboutQuery(realm, about), undefined);
|
|
524
|
+
// One row per revision, and a Horn can bring two of those, so the Horn is what is counted and the
|
|
525
|
+
// first row for each is the one read.
|
|
526
|
+
const firstRows = new Map();
|
|
527
|
+
for (const row of found.results.bindings) {
|
|
528
|
+
const hornIri = row["horn"]?.value;
|
|
529
|
+
if (isPresent(hornIri) && !firstRows.has(hornIri))
|
|
530
|
+
firstRows.set(hornIri, row);
|
|
531
|
+
}
|
|
532
|
+
if (firstRows.size === 0) {
|
|
533
|
+
const made = (await hornForRule({ select, realm, datastoreId, about })) ??
|
|
534
|
+
(await hornForPlan({ select, realm, datastoreId, about }));
|
|
535
|
+
return made === undefined
|
|
536
|
+
? Response.json({ horns: [], total: 0 }, { headers: noStore })
|
|
537
|
+
: Response.json({ horns: [{ horn: made, kept: false }], total: 0 }, { headers: noStore });
|
|
538
|
+
}
|
|
539
|
+
const wanted = [...firstRows.entries()].slice(0, asked);
|
|
540
|
+
const read = await Promise.all(wanted.map(async ([hornIri, row]) => {
|
|
541
|
+
const revision = row["revision"]?.value;
|
|
542
|
+
if (!isPresent(revision))
|
|
543
|
+
return undefined;
|
|
544
|
+
const one = await hornAtRevision({ select, realm, hornIri, revision, number: Number(row["number"]?.value) });
|
|
545
|
+
if (one === undefined)
|
|
546
|
+
return undefined;
|
|
547
|
+
const slug = row["slug"]?.value;
|
|
548
|
+
return {
|
|
549
|
+
horn: one.horn,
|
|
550
|
+
kept: true,
|
|
551
|
+
revision,
|
|
552
|
+
contentHash: await contentHash(one.stored),
|
|
553
|
+
...(isPresent(slug) ? { slug } : {}),
|
|
554
|
+
};
|
|
555
|
+
}));
|
|
556
|
+
// A revision that no longer reads as a Horn is left out rather than failing the lot: the others are
|
|
557
|
+
// still worth drawing, and `total` already says more exist.
|
|
558
|
+
return Response.json({ horns: read.filter(isPresent), total: firstRows.size }, { headers: noStore });
|
|
559
|
+
}
|
|
560
|
+
catch (caught) {
|
|
561
|
+
return refuse(502, caught instanceof Error ? caught.message : String(caught)).response;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
487
564
|
routes.get("/api/d/:datastoreId/horns/by/about", async (c) => {
|
|
488
565
|
const datastoreId = c.req.param("datastoreId");
|
|
489
566
|
const authorized = await authorize(c.req.raw, c.env, datastoreId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manta-eu/horns-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The Manta Horns worker as a library: Hono routes a Cloudflare host mounts on its own app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"sparqljs": "^3.7.4",
|
|
71
71
|
"typescript": "^5.9.0",
|
|
72
72
|
"zod": "^4.5.4",
|
|
73
|
-
"@manta-eu/horns": "0.2.
|
|
74
|
-
"@manta-eu/horns-kit": "0.2.
|
|
73
|
+
"@manta-eu/horns": "0.2.1",
|
|
74
|
+
"@manta-eu/horns-kit": "0.2.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@cloudflare/vite-plugin": "1.47.0",
|