feat(core,server): log feed, titles on changes, work titles on copies
GET log (engagements and listens, keyset over at/kind/id), listChanges title and intoTitle, listCopies workTitle, all via batched lookups. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5fa59310f3
commit
c2e8c83f86
@@ -8,6 +8,7 @@ import { engagementRoutes } from "./routes/engagements.ts";
|
||||
import { entryRoutes } from "./routes/entries.ts";
|
||||
import { exportRoutes } from "./routes/export.ts";
|
||||
import { listRoutes } from "./routes/lists.ts";
|
||||
import { logRoutes } from "./routes/log.ts";
|
||||
import { metaRoutes } from "./routes/meta.ts";
|
||||
import { sourceRoutes } from "./routes/sources.ts";
|
||||
import { workRoutes } from "./routes/works.ts";
|
||||
@@ -26,6 +27,7 @@ export function createApp(deps: AppDeps): App {
|
||||
...engagementRoutes,
|
||||
...workRoutes,
|
||||
...listRoutes,
|
||||
...logRoutes,
|
||||
...copyRoutes,
|
||||
...customFieldRoutes,
|
||||
...attachmentRoutes,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { listLog } from "@sttracker/core/log";
|
||||
import { z } from "zod";
|
||||
import { route } from "../http.ts";
|
||||
import { qInt } from "./schemas.ts";
|
||||
|
||||
export const logRoutes = [
|
||||
route({
|
||||
method: "GET",
|
||||
path: "log",
|
||||
summary: "Your Log: engagements and listens, newest first (keyset cursor).",
|
||||
auth: "any:read",
|
||||
query: z.object({ limit: qInt.optional(), cursor: z.string().max(2000).optional() }),
|
||||
handler: async ({ deps, user, query }) => listLog(deps.db, user.id, query),
|
||||
}),
|
||||
];
|
||||
@@ -329,6 +329,25 @@ forEachDialect("api", (getDb) => {
|
||||
expect((await call(app, "PATCH", "me/preferences", { cookie: ana, body: { ratingScale: "5" }, noCsrf: true })).status).toBe(403);
|
||||
});
|
||||
|
||||
test("log: engagements newest first with keyset paging; copies carry workTitle", async () => {
|
||||
const ana = await signUp(app, "ana");
|
||||
const e = (await (await call(app, "POST", "entries/quick", { cookie: ana, body: { title: "Dune" } })).json()) as { id: string; workId: string };
|
||||
for (const value of ["2026-01-02", "2026-03-04"]) {
|
||||
const r = await call(app, "POST", `entries/${e.id}/engagements`, { cookie: ana, body: { finishedAt: { value, precision: "day" } } });
|
||||
expect(r.status).toBe(201);
|
||||
}
|
||||
const page1 = (await (await call(app, "GET", "log?limit=1", { cookie: ana })).json()) as { items: { at: string; title: string; kind: string }[]; nextCursor: string };
|
||||
expect(page1.items).toEqual([expect.objectContaining({ kind: "engagement", at: "2026-03-04", title: "Dune" })]);
|
||||
const page2 = (await (await call(app, "GET", `log?limit=1&cursor=${page1.nextCursor}`, { cookie: ana })).json()) as { items: { at: string }[]; nextCursor: string | null };
|
||||
expect(page2.items.map((i) => i.at)).toEqual(["2026-01-02"]);
|
||||
expect(page2.nextCursor).toBeNull();
|
||||
expect((await call(app, "GET", "log?cursor=bad", { cookie: ana })).status).toBe(400);
|
||||
expect((await call(app, "GET", "log")).status).toBe(401);
|
||||
|
||||
await call(app, "POST", "copies", { cookie: ana, body: { workId: e.workId, format: "Paperback" } });
|
||||
expect(await (await call(app, "GET", "copies", { cookie: ana })).json()).toEqual([expect.objectContaining({ workTitle: "Dune", format: "Paperback" })]);
|
||||
});
|
||||
|
||||
test("JSON bodies need Content-Type application/json (415)", async () => {
|
||||
const res = await app.fetch(
|
||||
new Request(`${BASE}/auth/register`, {
|
||||
|
||||
Reference in New Issue
Block a user