| 1 | /**
|
|---|
| 2 | * Guardianship (FEP-633c) — the COMMITTED ward ↔ guardian relations
|
|---|
| 3 | * (ap_guardianships). Pending offers live in offers.js; a row here means the
|
|---|
| 4 | * handshake committed (§3.1.4). Every row is one relation seen from a LOCAL
|
|---|
| 5 | * site: role 'guardian' = the site guards other_uri; role 'ward' = other_uri
|
|---|
| 6 | * guards the site.
|
|---|
| 7 | */
|
|---|
| 8 | import db from '../../config/database.js';
|
|---|
| 9 |
|
|---|
| 10 | let _s = null;
|
|---|
| 11 | function stmts() {
|
|---|
| 12 | if (!_s) {
|
|---|
| 13 | _s = {
|
|---|
| 14 | commit: db.prepare(`INSERT INTO ap_guardianships (slug, role, other_uri, other_handle, status, offer_id, created_at)
|
|---|
| 15 | VALUES (?,?,?,?, 'accepted', ?, CURRENT_TIMESTAMP)
|
|---|
| 16 | ON CONFLICT(slug, role, other_uri) DO UPDATE SET status='accepted', offer_id=excluded.offer_id`),
|
|---|
| 17 | del: db.prepare('DELETE FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
|
|---|
| 18 | bySlugRole: db.prepare("SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND status='accepted' ORDER BY created_at DESC"),
|
|---|
| 19 | one: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
|
|---|
| 20 | };
|
|---|
| 21 | }
|
|---|
| 22 | return _s;
|
|---|
| 23 | }
|
|---|
| 24 |
|
|---|
| 25 | // ── Reads ────────────────────────────────────────────────────────────────
|
|---|
| 26 |
|
|---|
| 27 | /** Accepted guardian URIs of a local ward (feeds shaer:guardians). */
|
|---|
| 28 | export function listGuardians(slug) { return stmts().bySlugRole.all(slug, 'ward'); }
|
|---|
| 29 |
|
|---|
| 30 | /** Accepted wards of a local guardian (the wards queue). */
|
|---|
| 31 | export function listWards(slug) { return stmts().bySlugRole.all(slug, 'guardian'); }
|
|---|
| 32 |
|
|---|
| 33 | /** A site is a guardian once it stands in any accepted guardian relation. */
|
|---|
| 34 | export function isGuardian(slug) { return listWards(slug).length > 0; }
|
|---|
| 35 |
|
|---|
| 36 | export function getRelation(slug, role, otherUri) { return stmts().one.get(slug, role, otherUri); }
|
|---|
| 37 |
|
|---|
| 38 | // ── Writes (only the handshake commit lands here) ────────────────────────
|
|---|
| 39 |
|
|---|
| 40 | /** The local ward gains a guardian (commit, §3.1.4). */
|
|---|
| 41 | export function commitGuardianForWard(wardSlug, guardianUri, { handle = null, offerId = null } = {}) {
|
|---|
| 42 | stmts().commit.run(wardSlug, 'ward', guardianUri, handle, offerId);
|
|---|
| 43 | return stmts().one.get(wardSlug, 'ward', guardianUri);
|
|---|
| 44 | }
|
|---|
| 45 |
|
|---|
| 46 | /** The local guardian gains a ward (commit, §3.1.4). */
|
|---|
| 47 | export function commitWardForGuardian(guardianSlug, wardUri, { handle = null, offerId = null } = {}) {
|
|---|
| 48 | stmts().commit.run(guardianSlug, 'guardian', wardUri, handle, offerId);
|
|---|
| 49 | return stmts().one.get(guardianSlug, 'guardian', wardUri);
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | /** End a relation locally (Undo, §3.2 — federation of the Undo is Fase 4). */
|
|---|
| 53 | export function removeRelation(slug, role, otherUri) {
|
|---|
| 54 | stmts().del.run(slug, role, otherUri);
|
|---|
| 55 | return { ok: true };
|
|---|
| 56 | }
|
|---|
| 57 |
|
|---|
| 58 | // ── Actor document (FEP-633c §2) ─────────────────────────────────────────
|
|---|
| 59 |
|
|---|
| 60 | /**
|
|---|
| 61 | * Guardianship props for a local actor doc. `id` is the actor URI.
|
|---|
| 62 | * - shaer:guardians: accepted guardians of this ward (omitted when none, §2.1)
|
|---|
| 63 | * - shaer:isGuardian: true once the site guards anyone
|
|---|
| 64 | * - shaer:queues: the owner-only dashboard collections
|
|---|
| 65 | *
|
|---|
| 66 | * §1 mutual exclusion: a ward (has guardians) is never a guardian, so
|
|---|
| 67 | * shaer:isGuardian is suppressed if guardians exist; the offer path already
|
|---|
| 68 | * bars a ward from offering.
|
|---|
| 69 | */
|
|---|
| 70 | export function actorProps(id, slug) {
|
|---|
| 71 | const props = {
|
|---|
| 72 | 'shaer:queues': {
|
|---|
| 73 | offers: `${id}/queues/offers`,
|
|---|
| 74 | follows: `${id}/queues/follows`,
|
|---|
| 75 | // Both directions of §5.3, kept apart on purpose: a guardian must be able
|
|---|
| 76 | // to tell "someone wants to follow your ward" from "your ward wants to
|
|---|
| 77 | // follow someone". Same mechanism, opposite question, different words in
|
|---|
| 78 | // the interface (shaer-p729).
|
|---|
| 79 | outgoingFollows: `${id}/queues/outgoing-follows`,
|
|---|
| 80 | wards: `${id}/queues/wards`,
|
|---|
| 81 | guardians: `${id}/queues/guardians`,
|
|---|
| 82 | help: `${id}/queues/help`,
|
|---|
| 83 | },
|
|---|
| 84 | // NAAST de wachtrijen, niet erin. Alles onder shaer:queues wacht op een
|
|---|
| 85 | // antwoord; dit is wat er al besloten is, met de reden erbij (§4.2).
|
|---|
| 86 | // Geschiedenis onderbrengen bij een woord dat "wachtend" betekent maakt van
|
|---|
| 87 | // twee dingen één, en dat is precies de fout die de rest van deze module
|
|---|
| 88 | // net heeft opgeruimd.
|
|---|
| 89 | 'shaer:log': `${id}/log`,
|
|---|
| 90 | };
|
|---|
| 91 | const guardians = listGuardians(slug).map((r) => r.other_uri);
|
|---|
| 92 | if (guardians.length) {
|
|---|
| 93 | props['shaer:guardians'] = guardians; // a ward
|
|---|
| 94 | } else if (isGuardian(slug)) {
|
|---|
| 95 | props['shaer:isGuardian'] = true; // a guardian (never both, §1)
|
|---|
| 96 | }
|
|---|
| 97 | return props;
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | export default {
|
|---|
| 101 | listGuardians, listWards, isGuardian, getRelation,
|
|---|
| 102 | commitGuardianForWard, commitWardForGuardian, removeRelation, actorProps,
|
|---|
| 103 | };
|
|---|