Public: anyone can read this; the crew can change it.

src/core/repos.ts has the reads a display lists from: listAdminOwnedRepos, readableRepoIds, repoDatesFor and listAllReposByOwner #9303

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9273-src-core-repos-ts-has-the-reads-a-displa → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/9273-src-core-repos-ts-has-the-reads-a-displa was merged into main as 5dc23ad.

Review

Sign in to review this change.
2 files changed +164 −0
src/core/repos.ts +75
@@ -1364,3 +1364,78 @@ export function setDirectionIssue(
13641364 issueId,
13651365 );
13661366 }
1367
1368 // Reads a display lists from (display/known.ts, display/items.ts).
1369
1370 /**
1371 * When each repository was created and last pushed to (the newest `push`
1372 * event, null when there is none), for a list of forks: one query per chunk.
1373 */
1374 export function repoDatesFor(
1375 db: Database,
1376 repoIds: readonly string[],
1377 ): Map<string, { createdAt: string; pushedAt: string | null }> {
1378 const out = new Map<string, { createdAt: string; pushedAt: string | null }>();
1379 for (let i = 0; i < repoIds.length; i += 500) {
1380 const chunk = repoIds.slice(i, i + 500);
1381 const marks = chunk.map((_, j) => `?${j + 1}`).join(",");
1382 const rows = db
1383 .query<{ id: string; created_at: string; pushed_at: string | null }, string[]>(
1384 `SELECT repo.id, repo.created_at,
1385 (SELECT MAX(event.created_at) FROM event
1386 WHERE event.repo_id = repo.id AND event.kind = 'push') AS pushed_at
1387 FROM repo WHERE repo.id IN (${marks})`,
1388 )
1389 .all(...chunk);
1390 for (const r of rows)
1391 out.set(r.id, { createdAt: r.created_at, pushedAt: r.pushed_at });
1392 }
1393 return out;
1394 }
1395
1396 /**
1397 * Every repo one user or org owns, whoever looks: for a display, whose
1398 * showcased demos are published from private repositories too
1399 * (`display/items.ts`). Never a listing a person sees as repositories.
1400 */
1401 export function listAllReposByOwner(db: Database, ownerName: string, limit: number): Repo[] {
1402 return db
1403 .query<RepoRow, [string, number]>(`${SELECT} WHERE user.username = ?1 COLLATE NOCASE ORDER BY repo.id DESC LIMIT ?2`)
1404 .all(ownerName, limit)
1405 .map(toRepo);
1406 }
1407
1408 /**
1409 * Which of these repositories the viewer may read — `REPO_VISIBLE` (or
1410 * `PUBLIC_VISIBLE` signed out) for many rows in one query per chunk: the
1411 * rule every listing applies, so a row never links a page that would 404.
1412 */
1413 export function readableRepoIds(
1414 db: Database,
1415 viewer: User | undefined,

Nothing calls these four reads yet. Fine as a staged import, but the follow-up pull that adds the display callers should land next so they are not dead code.

1416 repoIds: readonly string[],
1417 ): Set<string> {
1418 const out = new Set<string>();
1419 const unique = [...new Set(repoIds)];
1420 const CHUNK = 400;
1421 for (let i = 0; i < unique.length; i += CHUNK) {
1422 const batch = unique.slice(i, i + CHUNK);
1423 const rows = db
1424 .query<{ id: string }, string[]>(
1425 `SELECT repo.id FROM repo
1426 WHERE repo.id IN (${batch.map((_, n) => `?${n + 2}`).join(",")})
1427 AND ${viewer ? REPO_VISIBLE : PUBLIC_VISIBLE}`,
1428 )
1429 .all(viewer?.id ?? "", ...batch);
1430 for (const row of rows) out.add(row.id);
1431 }
1432 return out;
1433 }
1434
1435 /** Every repository a site admin owns (a person, never an agent), its owner in standing. */
1436 export function listAdminOwnedRepos(db: Database): Repo[] {
1437 return db
1438 .query<RepoRow, []>(`${SELECT} WHERE user.is_admin = 1 AND user.is_agent = 0 AND ${OWNER_STANDING} ORDER BY owner_name, repo.name`)
1439 .all()
1440 .map(toRepo);
1441 }
test/repos-display-reads.test.ts added +89
@@ -0,0 +1,89 @@
1 /** The reads a display lists from: admin-owned repos, readable ids, dates, all repos by owner (#9273). */
2 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createUser, type User } from "../src/core/users.ts";
9 import {
10 listAdminOwnedRepos,
11 listAllReposByOwner,
12 readableRepoIds,
13 repoDatesFor,
14 type Repo,
15 } from "../src/core/repos.ts";
16 import { promote } from "./helpers/members.ts";
17
18 let dataDir: string;
19 let app: App;
20 let root: User;
21 let alice: User;
22 let bob: User;
23 const repos: Record<string, Repo> = {};
24
25 beforeAll(async () => {
26 dataDir = await mkdtemp(join(tmpdir(), "forge-display-reads-"));
27 app = createApp({ dataDir, port: 0 });
28 const mk = async (username: string, first = false) => {
29 const r = await createUser(app.db, {
30 origin: "seed", trusted: true, username, allowReserved: true,
31 email: `${username}@forge.local`, password: "a-good-passphrase", adminIfFirst: first,
32 });
33 if (!r.ok) throw new Error(r.error);
34 promote(app.db, r.value);
35 return r.value;
36 };
37 root = await mk("root", true);
38 alice = await mk("alice");
39 bob = await mk("bob");
40 const make = async (key: string, who: User, name: string, visibility: "public" | "private") => {
41 const r = await createRepo(app, who, { name, visibility });
42 if (!r.ok) throw new Error(`${key}: ${r.error}`);
43 repos[key] = r.value;
44 };
45 await make("rootPub", root, "rootpub", "public");
46 await make("rootPriv", root, "rootpriv", "private");
47 await make("a1", alice, "one", "public");
48 await make("a2", alice, "two", "private");
49 await make("a3", alice, "three", "public");
50 await make("bobPriv", bob, "bobs", "private");
51 });
52 afterAll(async () => {
53 app.db.close();
54 await rm(dataDir, { recursive: true, force: true });
55 });
56
57 describe("display reads", () => {
58 test("listAdminOwnedRepos lists only repositories an admin owns", () => {
59 const names = listAdminOwnedRepos(app.db).map((r) => `${r.ownerName}/${r.name}`);
60 expect(names.every((n) => n.startsWith("root/"))).toBe(true);
61 expect(names.sort()).toEqual(["root/rootpriv", "root/rootpub"]);
62 });
63
64 test("readableRepoIds excludes a private repository the viewer cannot read", () => {
65 const ids = Object.values(repos).map((r) => r.id);
66 const mine = readableRepoIds(app.db, alice, ids);
67 expect(mine.has(repos.a2!.id)).toBe(true);
68 expect(mine.has(repos.rootPub!.id)).toBe(true);
69 expect(mine.has(repos.bobPriv!.id)).toBe(false);
70 expect(mine.has(repos.rootPriv!.id)).toBe(false);
71 const anon = readableRepoIds(app.db, undefined, ids);
72 expect([...anon].sort()).toEqual([repos.rootPub!.id, repos.a1!.id, repos.a3!.id].sort());
73 expect(readableRepoIds(app.db, alice, []).size).toBe(0);
74 });
75
76 test("listAllReposByOwner honours its limit and ignores visibility", () => {
77 expect(listAllReposByOwner(app.db, "alice", 10).length).toBe(3);
78 expect(listAllReposByOwner(app.db, "ALICE", 2).length).toBe(2);
79 expect(listAllReposByOwner(app.db, "nobody", 5)).toEqual([]);
80 });
81
82 test("repoDatesFor returns one entry per id asked", () => {
83 const ids = [repos.a1!.id, repos.a2!.id, repos.a1!.id];
84 const dates = repoDatesFor(app.db, [...ids, "missing"]);
85 expect([...dates.keys()].sort()).toEqual([repos.a1!.id, repos.a2!.id].sort());
86 expect(dates.get(repos.a1!.id)!.createdAt).toBeTruthy();
87 expect(repoDatesFor(app.db, []).size).toBe(0);
88 });
89 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs