import { describe, it, expect, beforeEach } from "vitest"; import type { Database, RecordRow, RelationConfig, ReferenceConfig } from "../src/core/types"; import { recordsTableName } from "../src/core/types"; import { parseHydrateParams, resolveHydrates, resolveReferences } from "../src/core/router/hydrate"; import { createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; import { applyEvents } from "./helpers"; describe("parseHydrateParams", () => { const relations: Record = { rsvps: { collection: "rsvp", groupBy: "status" }, comments: { collection: "test.comment" }, }; const references: Record = { event: { collection: "event", field: "subject.uri" }, }; it("parses valid relation hydrate params", () => { const params = new URLSearchParams({ hydrateRsvps: "5", hydrateComments: "3" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({ rsvps: 5, comments: 3 }); expect(result.references.size).toBe(0); }); it("parses valid reference hydrate params", () => { const params = new URLSearchParams({ hydrateEvent: "true" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({}); expect(result.references.has("event")).toBe(true); }); it("parses reference hydrate with 1", () => { const params = new URLSearchParams({ hydrateEvent: "1" }); const result = parseHydrateParams(params, relations, references); expect(result.references.has("event")).toBe(true); }); it("ignores unrelated params", () => { const params = new URLSearchParams({ hydrateFoo: "5", limit: "10" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({}); expect(result.references.size).toBe(0); }); it("ignores invalid numbers", () => { const params = new URLSearchParams({ hydrateRsvps: "abc" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({}); }); it("ignores zero and negative", () => { const params = new URLSearchParams({ hydrateRsvps: "0", hydrateComments: "-1" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({}); }); it("returns empty for no hydrate params", () => { const params = new URLSearchParams({ limit: "50" }); const result = parseHydrateParams(params, relations, references); expect(result.relations).toEqual({}); expect(result.references.size).toBe(0); }); }); describe("resolveHydrates", () => { let db: Database; beforeEach(async () => { db = await createTestDbWithSchema(); }); it("returns empty when no hydrations requested", async () => { const result = await resolveHydrates(db, {}, {}, []); expect(result).toEqual({}); }); it("returns empty when no records", async () => { const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 5 }, []); expect(result).toEqual({}); }); it("hydrates related records", async () => { const eventUri = "at://did:plc:test/community.lexicon.calendar.event/evt1"; // Insert event await applyEvents(db, [makeEvent({ uri: eventUri, rkey: "evt1", time_us: 1000 })]); // Insert RSVPs for (let i = 0; i < 3; i++) { await applyEvents(db, [ makeEvent({ uri: `at://did:plc:user${i}/community.lexicon.calendar.rsvp/r${i}`, did: `did:plc:user${i}`, collection: "rsvp", rkey: `r${i}`, record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000 + i, }), ]); } const eventRow = await db .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); expect(result[eventUri]).toBeDefined(); expect(result[eventUri].rsvps).toBeDefined(); expect(result[eventUri].rsvps["going"]).toHaveLength(3); }); it("respects hydrate limit", async () => { const eventUri = "at://did:plc:test/community.lexicon.calendar.event/evt1"; await applyEvents(db, [makeEvent({ uri: eventUri, rkey: "evt1", time_us: 1000 })]); for (let i = 0; i < 5; i++) { await applyEvents(db, [ makeEvent({ uri: `at://did:plc:user${i}/community.lexicon.calendar.rsvp/r${i}`, did: `did:plc:user${i}`, collection: "rsvp", rkey: `r${i}`, record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000 + i, }), ]); } const eventRow = await db .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 2 }, [eventRow!]); expect(result[eventUri].rsvps["going"].length).toBeLessThanOrEqual(2); }); it("hydrates with match: 'did'", async () => { const did = "did:plc:shareduser"; const eventUri = `at://${did}/community.lexicon.calendar.event/evt1`; // Insert event owned by this DID await applyEvents(db, [makeEvent({ uri: eventUri, did, rkey: "evt1", time_us: 1000 })]); // Insert a related record whose "author" field contains the parent's DID await applyEvents(db, [ makeEvent({ uri: `at://did:plc:other/community.lexicon.calendar.rsvp/r1`, did: "did:plc:other", collection: "rsvp", rkey: "r1", record: { author: did, status: "going" }, time_us: 2000, }), ]); const eventRow = await db .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); // match: "did" means matchValues are parent DIDs, and field points to where the DID is stored const relations: Record = { rsvps: { collection: "rsvp", match: "did", field: "author", groupBy: "status" }, }; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); expect(result[eventUri]).toBeDefined(); expect(result[eventUri].rsvps).toBeDefined(); expect(result[eventUri].rsvps["going"]).toHaveLength(1); }); it("resolves reference: event onto rsvp (child→parent via subject.uri)", async () => { const eventUri = "at://did:plc:test/community.lexicon.calendar.event/evt1"; // Insert event await applyEvents(db, [ makeEvent({ uri: eventUri, rkey: "evt1", record: { name: "My Event" }, time_us: 1000 }), ]); // Insert RSVP pointing at the event const rsvpUri = "at://did:plc:user1/community.lexicon.calendar.rsvp/r1"; await applyEvents(db, [ makeEvent({ uri: rsvpUri, did: "did:plc:user1", collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000, }), ]); const rsvpRow = await db .prepare(`SELECT * FROM ${recordsTableName("rsvp")} WHERE uri = ?`) .bind(rsvpUri) .first(); const references = TEST_CONFIG.collections["rsvp"].references!; const result = await resolveReferences(db, references, new Set(["event"]), [rsvpRow!]); expect(result[rsvpUri]).toBeDefined(); expect(result[rsvpUri].event).toBeDefined(); // reference returns a single object, not an array expect(result[rsvpUri].event.uri).toBe(eventUri); }); it("resolves references onto multiple rsvps", async () => { const eventUri1 = "at://did:plc:test/community.lexicon.calendar.event/evt1"; const eventUri2 = "at://did:plc:test/community.lexicon.calendar.event/evt2"; // Insert two events await applyEvents(db, [ makeEvent({ uri: eventUri1, rkey: "evt1", record: { name: "Event 1" }, time_us: 1000 }), makeEvent({ uri: eventUri2, rkey: "evt2", record: { name: "Event 2" }, time_us: 1001 }), ]); // Insert RSVPs pointing at different events const rsvpUri1 = "at://did:plc:user1/community.lexicon.calendar.rsvp/r1"; const rsvpUri2 = "at://did:plc:user2/community.lexicon.calendar.rsvp/r2"; await applyEvents(db, [ makeEvent({ uri: rsvpUri1, did: "did:plc:user1", collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri1 }, status: "going" }, time_us: 2000, }), makeEvent({ uri: rsvpUri2, did: "did:plc:user2", collection: "rsvp", rkey: "r2", record: { subject: { uri: eventUri2 }, status: "interested" }, time_us: 2001, }), ]); const rsvpRows = await db .prepare(`SELECT * FROM ${recordsTableName("rsvp")} ORDER BY time_us DESC`) .all(); const references = TEST_CONFIG.collections["rsvp"].references!; const result = await resolveReferences(db, references, new Set(["event"]), rsvpRows.results!); // Each RSVP should have its event resolved expect(result[rsvpUri1]?.event).toBeDefined(); expect(result[rsvpUri2]?.event).toBeDefined(); expect(result[rsvpUri1].event.uri).toBe(eventUri1); expect(result[rsvpUri2].event.uri).toBe(eventUri2); }); it("groups into 'other' when groupBy value is null", async () => { const eventUri = "at://did:plc:test/community.lexicon.calendar.event/evt1"; await applyEvents(db, [makeEvent({ uri: eventUri, rkey: "evt1", time_us: 1000 })]); // Insert RSVP without a status field — groupBy "status" should fall back to "other" await applyEvents(db, [ makeEvent({ uri: "at://did:plc:user1/community.lexicon.calendar.rsvp/r1", did: "did:plc:user1", collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri } }, time_us: 2000, }), ]); const eventRow = await db .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); expect(result[eventUri].rsvps).toBeDefined(); expect(result[eventUri].rsvps["other"]).toHaveLength(1); }); });