pierre-review 0.1.20 → 0.1.22

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.
@@ -17,6 +17,12 @@ const EVENT_TYPES = [
17
17
  'commit_pushed',
18
18
  ];
19
19
  const PR_STATUSES = ['draft', 'open', 'merged', 'closed'];
20
+ const REVIEW_FILTER_STATES = [
21
+ 'approved',
22
+ 'changes_requested',
23
+ 'commented',
24
+ 'dismissed',
25
+ ];
20
26
  function parseIntList(raw) {
21
27
  if (!raw)
22
28
  return null;
@@ -48,6 +54,18 @@ function parseStatuses(raw) {
48
54
  .map((s) => s.trim())
49
55
  .filter((s) => allowed.has(s));
50
56
  }
57
+ // Absent (undefined) → null = no review-verdict filter (show all). Present, even
58
+ // empty ("") → an explicit (possibly empty) set, so deselecting every verdict hides
59
+ // all review markers rather than falling back to "all". Mirrors parseStatuses.
60
+ function parseReviewStates(raw) {
61
+ if (raw === undefined)
62
+ return null;
63
+ const allowed = new Set(REVIEW_FILTER_STATES);
64
+ return raw
65
+ .split(',')
66
+ .map((s) => s.trim())
67
+ .filter((s) => allowed.has(s));
68
+ }
51
69
  function parseDate(raw, fallback) {
52
70
  if (!raw)
53
71
  return fallback;
@@ -66,6 +84,7 @@ export async function timelineRoutes(app) {
66
84
  userIds: parseIntList(q.userIds),
67
85
  types: parseTypes(q.types),
68
86
  statuses: parseStatuses(q.statuses),
87
+ reviewStates: parseReviewStates(q.reviewStates),
69
88
  excludeBots: q.excludeBots === 'true',
70
89
  excludeStale: q.excludeStale === 'true',
71
90
  };
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { and, asc, count, desc, eq, exists, gt, gte, inArray, isNotNull, isNull, lte, notInArray, or, sql, } from 'drizzle-orm';
2
+ import { and, asc, count, desc, eq, exists, gt, gte, inArray, isNotNull, isNull, lte, ne, notInArray, or, sql, } from 'drizzle-orm';
3
3
  // Local copy of the shared `REASON_PRIORITY` value constant. `@pierre-review/shared`
4
4
  // is a types-only workspace package that is NOT shipped in the published tarball,
5
5
  // so the backend must only `import type` from it. Keep in sync with packages/shared.
@@ -222,7 +222,7 @@ function mapTimelinePr(p, counts, tr) {
222
222
  };
223
223
  }
224
224
  export async function getTimeline(filters) {
225
- const { accountId, from, to, repoIds, userIds, types, statuses, excludeBots, excludeStale, } = filters;
225
+ const { accountId, from, to, repoIds, userIds, types, statuses, reviewStates, excludeBots, excludeStale, } = filters;
226
226
  // ---- PRs that overlap the window ----
227
227
  const prConds = [
228
228
  eq(pullRequests.accountId, accountId),
@@ -280,6 +280,19 @@ export async function getTimeline(filters) {
280
280
  .from(pullRequests)
281
281
  .where(and(eq(pullRequests.id, events.prId), prStatusWhere(statuses)))));
282
282
  }
283
+ // Review-verdict filter: keep every NON-review event; for review_submitted events,
284
+ // keep only those whose referenced review's state is selected. An empty selection
285
+ // drops all review markers (the review row exists but no verdict matches). null =
286
+ // no filter. Pure-reviewer rows vanish when their verdict is deselected because the
287
+ // event is removed here (not just hidden client-side), so no empty row lingers.
288
+ if (reviewStates) {
289
+ evConds.push(reviewStates.length === 0
290
+ ? ne(events.type, 'review_submitted')
291
+ : or(ne(events.type, 'review_submitted'), exists(db
292
+ .select({ x: sql `1` })
293
+ .from(reviews)
294
+ .where(and(eq(reviews.id, events.refId), eq(events.refTable, 'reviews'), inArray(reviews.state, reviewStates))))));
295
+ }
283
296
  // Likewise drop a stale open PR's own events (only ever lifecycle markers, since
284
297
  // by definition it has no activity events in-window) so its contributor row can
285
298
  // disappear instead of lingering empty. Keep events with no PR (defensive).
@@ -310,21 +323,40 @@ export async function getTimeline(filters) {
310
323
  for (const r of rows)
311
324
  reviewStateById.set(r.id, r.state);
312
325
  }
313
- const timelineEvents = evRows.map((e) => ({
314
- id: e.id,
315
- repoId: e.repoId,
316
- actorId: e.actorId,
317
- prId: e.prId,
318
- type: e.type,
319
- occurredAt: e.occurredAt.toISOString(),
320
- threadId: e.type === 'review_comment' && e.refTable === 'review_threads'
321
- ? e.refId
322
- : null,
323
- refId: e.refId,
324
- reviewState: e.type === 'review_submitted' && e.refTable === 'reviews' && e.refId != null
325
- ? (reviewStateById.get(e.refId) ?? null)
326
- : null,
327
- }));
326
+ // Batch-load the derived state of each review_comment event's thread, so the
327
+ // timeline's "Threads" filter can narrow markers to a specific thread state
328
+ // (e.g. only resolved) instead of every comment on a PR that has a matching
329
+ // thread. A single keyed lookup; the thread's id is the event's refId.
330
+ const threadRefIds = evRows
331
+ .filter((e) => e.type === 'review_comment' && e.refTable === 'review_threads' && e.refId != null)
332
+ .map((e) => e.refId);
333
+ const threadStateById = new Map();
334
+ if (threadRefIds.length > 0) {
335
+ const rows = await db
336
+ .select({ id: reviewThreads.id, state: reviewThreads.derivedState })
337
+ .from(reviewThreads)
338
+ .where(inArray(reviewThreads.id, threadRefIds))
339
+ .execute();
340
+ for (const r of rows)
341
+ threadStateById.set(r.id, r.state);
342
+ }
343
+ const timelineEvents = evRows.map((e) => {
344
+ const threadId = e.type === 'review_comment' && e.refTable === 'review_threads' ? e.refId : null;
345
+ return {
346
+ id: e.id,
347
+ repoId: e.repoId,
348
+ actorId: e.actorId,
349
+ prId: e.prId,
350
+ type: e.type,
351
+ occurredAt: e.occurredAt.toISOString(),
352
+ threadId,
353
+ derivedState: threadId != null ? (threadStateById.get(threadId) ?? null) : null,
354
+ refId: e.refId,
355
+ reviewState: e.type === 'review_submitted' && e.refTable === 'reviews' && e.refId != null
356
+ ? (reviewStateById.get(e.refId) ?? null)
357
+ : null,
358
+ };
359
+ });
328
360
  return { prs, events: timelineEvents };
329
361
  }
330
362
  export async function getOpenPrs(filters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pierre-review",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
5
5
  "type": "module",
6
6
  "author": "Alex Wakeman",