@rudderhq/server 0.2.9-canary.8 → 0.2.9

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.
Files changed (86) hide show
  1. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +2 -2
  2. package/dist/routes/messenger.d.ts.map +1 -1
  3. package/dist/routes/messenger.js +6 -1
  4. package/dist/routes/messenger.js.map +1 -1
  5. package/dist/services/messenger.d.ts +6 -1
  6. package/dist/services/messenger.d.ts.map +1 -1
  7. package/dist/services/messenger.js +403 -231
  8. package/dist/services/messenger.js.map +1 -1
  9. package/package.json +13 -13
  10. package/ui-dist/assets/{_basePickBy-CpPt_W_6.js → _basePickBy-DvVj7rIS.js} +1 -1
  11. package/ui-dist/assets/{_baseUniq-DhSRH2al.js → _baseUniq-7APKp32S.js} +1 -1
  12. package/ui-dist/assets/{arc-CTJsGqKU.js → arc-BOK9kp7K.js} +1 -1
  13. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-q1l0BEmJ.js → architectureDiagram-2XIMDMQ5-CPWEJ5-Q.js} +1 -1
  14. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-BJzhf8tv.js → blockDiagram-WCTKOSBZ-1p1VKW9q.js} +1 -1
  15. package/ui-dist/assets/{c4Diagram-IC4MRINW-Dsvl_y8w.js → c4Diagram-IC4MRINW-BwQGsEPe.js} +1 -1
  16. package/ui-dist/assets/channel-C-lsMG4t.js +1 -0
  17. package/ui-dist/assets/{chunk-4BX2VUAB-BMrY-qAF.js → chunk-4BX2VUAB-nW7FE3wg.js} +1 -1
  18. package/ui-dist/assets/{chunk-55IACEB6-BGx4hYzL.js → chunk-55IACEB6-dmAMmzcP.js} +1 -1
  19. package/ui-dist/assets/{chunk-FMBD7UC4-B19LpjrO.js → chunk-FMBD7UC4-7kIxdw00.js} +1 -1
  20. package/ui-dist/assets/{chunk-JSJVCQXG-BcWVRCSn.js → chunk-JSJVCQXG-Dh5JJrKH.js} +1 -1
  21. package/ui-dist/assets/{chunk-KX2RTZJC-DCHaLTgt.js → chunk-KX2RTZJC-DpU5afUI.js} +1 -1
  22. package/ui-dist/assets/{chunk-NQ4KR5QH-jz_GzT4N.js → chunk-NQ4KR5QH-PTSr9534.js} +1 -1
  23. package/ui-dist/assets/{chunk-QZHKN3VN-B2tyTuvA.js → chunk-QZHKN3VN-UPJs2alU.js} +1 -1
  24. package/ui-dist/assets/{chunk-WL4C6EOR-CZh-0Jyo.js → chunk-WL4C6EOR-BN_ENVee.js} +1 -1
  25. package/ui-dist/assets/classDiagram-VBA2DB6C-Cq3o3fUE.js +1 -0
  26. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-Cq3o3fUE.js +1 -0
  27. package/ui-dist/assets/clone-B2MEXD1I.js +1 -0
  28. package/ui-dist/assets/{cose-bilkent-S5V4N54A-DWhfCZEc.js → cose-bilkent-S5V4N54A-CgScQJik.js} +1 -1
  29. package/ui-dist/assets/{dagre-KLK3FWXG-CVOylwDE.js → dagre-KLK3FWXG-BPckIGrY.js} +1 -1
  30. package/ui-dist/assets/{diagram-E7M64L7V-g7MrjlZ3.js → diagram-E7M64L7V-DuGPsP2X.js} +1 -1
  31. package/ui-dist/assets/{diagram-IFDJBPK2-BMReBEDi.js → diagram-IFDJBPK2-CJK8D8DE.js} +1 -1
  32. package/ui-dist/assets/{diagram-P4PSJMXO-D5ZQkK9R.js → diagram-P4PSJMXO-CQYGYERn.js} +1 -1
  33. package/ui-dist/assets/{erDiagram-INFDFZHY-CelY5qIl.js → erDiagram-INFDFZHY-B1t5SPtA.js} +1 -1
  34. package/ui-dist/assets/{flowDiagram-PKNHOUZH-D2S3D6LD.js → flowDiagram-PKNHOUZH-ol67OiZa.js} +1 -1
  35. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-VeaC-Osw.js → ganttDiagram-A5KZAMGK-C0IybBSk.js} +1 -1
  36. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-Q0OwIdnh.js → gitGraphDiagram-K3NZZRJ6-CDI1KyGT.js} +1 -1
  37. package/ui-dist/assets/{graph-CTaTokeM.js → graph-DfbaTV8H.js} +1 -1
  38. package/ui-dist/assets/{index-SfR0fOjF.js → index-5VZyjxqG.js} +1 -1
  39. package/ui-dist/assets/{index-4snCfFLB.js → index-B4b9dJCZ.js} +1 -1
  40. package/ui-dist/assets/{index-Bi9jz_s1.js → index-BY0rvsQr.js} +1 -1
  41. package/ui-dist/assets/{index-CG8y9sHi.js → index-BbaZbFmu.js} +1 -1
  42. package/ui-dist/assets/{index-BwOqVTOX.js → index-BgWchnYK.js} +1 -1
  43. package/ui-dist/assets/{index-D_auIzT1.js → index-BkjRMCKV.js} +1 -1
  44. package/ui-dist/assets/{index-Bl6h3llH.js → index-Bp7FTWBa.js} +1 -1
  45. package/ui-dist/assets/{index-ffO4xnCv.js → index-BuQZXWs_.js} +1 -1
  46. package/ui-dist/assets/{index-B47wczcP.js → index-BwtxtbOn.js} +1 -1
  47. package/ui-dist/assets/{index-D1zkQEZD.js → index-C-8bU1a2.js} +1 -1
  48. package/ui-dist/assets/{index-DClnjK9T.js → index-C20tXHKr.js} +1 -1
  49. package/ui-dist/assets/{index-CZAxrPBR.js → index-CCoACxoq.js} +1 -1
  50. package/ui-dist/assets/{index-CCBwwSyP.js → index-CH1DDF-L.js} +1 -1
  51. package/ui-dist/assets/{index-CaHZbzOX.js → index-CQyjEt4k.js} +1 -1
  52. package/ui-dist/assets/{index-BGF3_9Wf.css → index-ChrDf7Tl.css} +1 -1
  53. package/ui-dist/assets/{index-BOOKoZaA.js → index-CiGhmrvv.js} +361 -356
  54. package/ui-dist/assets/{index-BVJS_4mL.js → index-CmWiZykS.js} +1 -1
  55. package/ui-dist/assets/{index-DYOk9J94.js → index-D37jXRBz.js} +1 -1
  56. package/ui-dist/assets/{index-Cjgq8ZJd.js → index-D7s_NX_D.js} +1 -1
  57. package/ui-dist/assets/{index-m5E0TZ2G.js → index-DNz3Dwni.js} +1 -1
  58. package/ui-dist/assets/{index-C6ZpX6D_.js → index-DYl1iDYY.js} +1 -1
  59. package/ui-dist/assets/{index-zzAQchXU.js → index-DdSgfd5C.js} +1 -1
  60. package/ui-dist/assets/{index-Bmio9tSL.js → index-DiLLpUas.js} +1 -1
  61. package/ui-dist/assets/{index-B3XzR0JX.js → index-yqabeuoc.js} +1 -1
  62. package/ui-dist/assets/{infoDiagram-LFFYTUFH-UuxdrO-4.js → infoDiagram-LFFYTUFH-Z3ElH_zP.js} +1 -1
  63. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-DfewMpzx.js → ishikawaDiagram-PHBUUO56-CoatViQz.js} +1 -1
  64. package/ui-dist/assets/{journeyDiagram-4ABVD52K-CyElHPHv.js → journeyDiagram-4ABVD52K-hhkgYz-z.js} +1 -1
  65. package/ui-dist/assets/{kanban-definition-K7BYSVSG-DV2eugya.js → kanban-definition-K7BYSVSG-Dkm2pUlW.js} +1 -1
  66. package/ui-dist/assets/{layout-DhyNWUBq.js → layout-Bf6FVhnF.js} +1 -1
  67. package/ui-dist/assets/{linear-CurqvqgD.js → linear-VR2Wuqio.js} +1 -1
  68. package/ui-dist/assets/{mermaid.core-DVY9qjLB.js → mermaid.core-BRZPHgF_.js} +4 -4
  69. package/ui-dist/assets/{mindmap-definition-YRQLILUH-dlgLJK0i.js → mindmap-definition-YRQLILUH-RS2G-uEV.js} +1 -1
  70. package/ui-dist/assets/{pieDiagram-SKSYHLDU-OWc7n4zl.js → pieDiagram-SKSYHLDU-CiIiai5w.js} +1 -1
  71. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-TrF2oXXY.js → quadrantDiagram-337W2JSQ-CW5xTip3.js} +1 -1
  72. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-Cg5dg9aA.js → requirementDiagram-Z7DCOOCP-CGHA107c.js} +1 -1
  73. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-QT66utjW.js → sankeyDiagram-WA2Y5GQK-B_tCRdvR.js} +1 -1
  74. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-BZBj2U96.js → sequenceDiagram-2WXFIKYE-DkQVsPbO.js} +1 -1
  75. package/ui-dist/assets/{stateDiagram-RAJIS63D-DLC9Plgu.js → stateDiagram-RAJIS63D-B1FtfiY9.js} +1 -1
  76. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-Bb4gItK8.js +1 -0
  77. package/ui-dist/assets/{timeline-definition-YZTLITO2-BKO4P0dR.js → timeline-definition-YZTLITO2-CmsH7kLP.js} +1 -1
  78. package/ui-dist/assets/{treemap-KZPCXAKY-CHu0FJn4.js → treemap-KZPCXAKY-Cf3Q5FzZ.js} +1 -1
  79. package/ui-dist/assets/{vennDiagram-LZ73GAT5-D5y_dnIJ.js → vennDiagram-LZ73GAT5-hNH0ehQ2.js} +1 -1
  80. package/ui-dist/assets/{xychartDiagram-JWTSCODW-ir_gQkq3.js → xychartDiagram-JWTSCODW-CL69JgaO.js} +1 -1
  81. package/ui-dist/index.html +2 -2
  82. package/ui-dist/assets/channel-b-0sodEj.js +0 -1
  83. package/ui-dist/assets/classDiagram-VBA2DB6C-C-PuZxuu.js +0 -1
  84. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-C-PuZxuu.js +0 -1
  85. package/ui-dist/assets/clone-CyENzMzj.js +0 -1
  86. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-VuzuaYLO.js +0 -1
@@ -1,10 +1,10 @@
1
- import { and, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm";
2
- import { activityLog, approvalComments, approvals, agents, authUsers, heartbeatRuns, issueComments, issues, joinRequests, messengerThreadUserStates, } from "@rudderhq/db";
1
+ import { and, desc, eq, gt, inArray, sql } from "drizzle-orm";
2
+ import { activityLog, approvalComments, approvals, agents, authUsers, heartbeatRuns, issueComments, issueFollows, issues, joinRequests, messengerThreadUserStates, } from "@rudderhq/db";
3
3
  import { formatMessengerPreview, formatMessengerTitle, } from "@rudderhq/shared";
4
- import { issueService } from "./issues.js";
5
4
  import { chatService } from "./chats.js";
6
5
  import { budgetService } from "./budgets.js";
7
6
  import { redactEventPayload } from "../redaction.js";
7
+ import { conflict } from "../errors.js";
8
8
  const ISSUE_ACTIVITY_ACTIONS = [
9
9
  "issue.updated",
10
10
  "issue.approval_linked",
@@ -18,32 +18,8 @@ const ISSUE_ACTIVITY_ACTIONS = [
18
18
  "heartbeat.retried",
19
19
  ];
20
20
  const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending"]);
21
- const ISSUE_UPDATE_METADATA_KEYS = new Set([
22
- "identifier",
23
- "issueIdentifier",
24
- "_previous",
25
- "source",
26
- "reopened",
27
- "reopenedFrom",
28
- "normalizedFromStatus",
29
- "normalizedReason",
30
- ]);
31
- function issueUpdatedChangedKeys(details) {
32
- if (!details)
33
- return [];
34
- return Object.keys(details).filter((key) => !ISSUE_UPDATE_METADATA_KEYS.has(key));
35
- }
36
- function isDescriptionOnlyIssueUpdate(activity) {
37
- if (activity.action !== "issue.updated")
38
- return false;
39
- const changedKeys = issueUpdatedChangedKeys(activity.details);
40
- return changedKeys.length === 1 && changedKeys[0] === "description";
41
- }
42
- function shouldNotifyIssueActivity(activity) {
43
- if (isDescriptionOnlyIssueUpdate(activity))
44
- return false;
45
- return true;
46
- }
21
+ const DEFAULT_ISSUE_THREAD_DETAIL_LIMIT = 50;
22
+ const MAX_ISSUE_THREAD_DETAIL_LIMIT = 100;
47
23
  function truncate(value, max = 140) {
48
24
  return formatMessengerPreview(value, { max });
49
25
  }
@@ -75,6 +51,40 @@ function compareChronologicalActivity(a, b) {
75
51
  return aTime - bTime;
76
52
  return a.title.localeCompare(b.title);
77
53
  }
54
+ function normalizeIssueThreadLimit(limit) {
55
+ if (typeof limit !== "number" || !Number.isFinite(limit))
56
+ return DEFAULT_ISSUE_THREAD_DETAIL_LIMIT;
57
+ return Math.min(MAX_ISSUE_THREAD_DETAIL_LIMIT, Math.max(1, Math.floor(limit)));
58
+ }
59
+ function encodeIssueThreadCursor(entry) {
60
+ const payload = {
61
+ activityAt: entry.latestActivityAt.toISOString(),
62
+ issueId: entry.issue.id,
63
+ };
64
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
65
+ }
66
+ function decodeIssueThreadCursor(cursor) {
67
+ if (!cursor)
68
+ return null;
69
+ try {
70
+ const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
71
+ if (typeof decoded.activityAt !== "string" || Number.isNaN(new Date(decoded.activityAt).getTime()))
72
+ return null;
73
+ if (typeof decoded.issueId !== "string" || decoded.issueId.length === 0)
74
+ return null;
75
+ return { activityAt: decoded.activityAt, issueId: decoded.issueId };
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ function compareIssueThreadEntriesChronological(a, b) {
82
+ const aTime = a.latestActivityAt.getTime();
83
+ const bTime = b.latestActivityAt.getTime();
84
+ if (aTime !== bTime)
85
+ return aTime - bTime;
86
+ return a.issue.id.localeCompare(b.issue.id);
87
+ }
78
88
  function threadKeyForChat(conversationId) {
79
89
  return `chat:${conversationId}`;
80
90
  }
@@ -181,9 +191,6 @@ function summarizeIssueActivity(activity, issue) {
181
191
  return `${issue.title} updated`;
182
192
  }
183
193
  }
184
- function isSelfAuthoredComment(comment, userId) {
185
- return comment.authorUserId === userId;
186
- }
187
194
  function issueCommentAuthorLabel(comment, currentUserId) {
188
195
  if (!comment)
189
196
  return null;
@@ -196,9 +203,6 @@ function issueCommentAuthorLabel(comment, currentUserId) {
196
203
  }
197
204
  return "System";
198
205
  }
199
- function isSelfAuthoredActivity(activity, userId) {
200
- return activity.actorType === "user" && activity.actorId === userId;
201
- }
202
206
  function summarizeApprovalPayload(approval) {
203
207
  const payload = redactEventPayload(approval.payload);
204
208
  if (!payload)
@@ -501,227 +505,395 @@ async function lastReadAtForThread(db, orgId, userId, threadKey, threadStates) {
501
505
  return (await states).get(threadKey)?.lastReadAt ?? null;
502
506
  }
503
507
  export function messengerService(db) {
504
- const issuesSvc = issueService(db);
505
508
  const chatsSvc = chatService(db);
506
509
  const budgetsSvc = budgetService(db);
507
- async function loadIssueUniverse(orgId, userId) {
508
- const [followRows, trackedRows] = await Promise.all([
509
- issuesSvc.listFollows(orgId, userId),
510
- db
511
- .select({
512
- id: issues.id,
513
- orgId: issues.orgId,
514
- title: issues.title,
515
- status: issues.status,
516
- priority: issues.priority,
517
- assigneeUserId: issues.assigneeUserId,
518
- reviewerUserId: issues.reviewerUserId,
519
- createdByUserId: issues.createdByUserId,
520
- identifier: issues.identifier,
521
- updatedAt: issues.updatedAt,
522
- })
523
- .from(issues)
524
- .where(and(eq(issues.orgId, orgId), or(eq(issues.assigneeUserId, userId), eq(issues.createdByUserId, userId), eq(issues.reviewerUserId, userId)), isNull(issues.hiddenAt)))
525
- .orderBy(desc(issues.updatedAt)),
526
- ]);
527
- const universe = new Map();
528
- for (const row of followRows) {
529
- universe.set(row.issueId, {
530
- ...row.issue,
531
- followed: true,
532
- assigned: row.issue.assigneeUserId === userId,
533
- });
534
- }
535
- for (const row of trackedRows) {
536
- const existing = universe.get(row.id);
537
- universe.set(row.id, {
538
- ...row,
539
- followed: existing?.followed ?? false,
540
- assigned: row.assigneeUserId === userId,
541
- });
542
- }
543
- return Array.from(universe.values());
510
+ const issueActionSqlList = sql.join(ISSUE_ACTIVITY_ACTIONS.map((action) => sql `${action}`), sql `, `);
511
+ function issueDescriptionOnlyActivitySql(alias) {
512
+ return sql `(
513
+ ${sql.raw(`${alias}.action`)} = 'issue.updated'
514
+ and jsonb_typeof(${sql.raw(`${alias}.details`)}) = 'object'
515
+ and ${sql.raw(`${alias}.details`)} ? 'description'
516
+ and not exists (
517
+ select 1
518
+ from jsonb_object_keys(${sql.raw(`${alias}.details`)}) as detail_key(key)
519
+ where detail_key.key not in (
520
+ 'description',
521
+ 'identifier',
522
+ 'issueIdentifier',
523
+ '_previous',
524
+ 'source',
525
+ 'reopened',
526
+ 'reopenedFrom',
527
+ 'normalizedFromStatus',
528
+ 'normalizedReason'
529
+ )
530
+ )
531
+ )`;
532
+ }
533
+ function issueEntryRowsQuery(orgId, userId, tail = sql ``) {
534
+ const descriptionOnlyActivity = issueDescriptionOnlyActivitySql("activity_row");
535
+ const externalDescriptionOnlyActivity = issueDescriptionOnlyActivitySql("external_activity_row");
536
+ return sql `
537
+ with tracked_issue_ids as (
538
+ select ${issues.id} as id
539
+ from ${issues}
540
+ where ${issues.orgId} = ${orgId}
541
+ and ${issues.hiddenAt} is null
542
+ and ${issues.assigneeUserId} = ${userId}
543
+ union
544
+ select ${issues.id} as id
545
+ from ${issues}
546
+ where ${issues.orgId} = ${orgId}
547
+ and ${issues.hiddenAt} is null
548
+ and ${issues.createdByUserId} = ${userId}
549
+ union
550
+ select ${issues.id} as id
551
+ from ${issues}
552
+ where ${issues.orgId} = ${orgId}
553
+ and ${issues.hiddenAt} is null
554
+ and ${issues.reviewerUserId} = ${userId}
555
+ union
556
+ select ${issueFollows.issueId} as id
557
+ from ${issueFollows}
558
+ inner join ${issues} followed_issue
559
+ on followed_issue.id = ${issueFollows.issueId}
560
+ and followed_issue.org_id = ${issueFollows.orgId}
561
+ where ${issueFollows.orgId} = ${orgId}
562
+ and ${issueFollows.userId} = ${userId}
563
+ and followed_issue.hidden_at is null
564
+ ),
565
+ issue_entries as (
566
+ select
567
+ issue_row.id as id,
568
+ issue_row.title as title,
569
+ issue_row.status as status,
570
+ issue_row.priority as priority,
571
+ issue_row.assignee_user_id as "assigneeUserId",
572
+ issue_row.reviewer_user_id as "reviewerUserId",
573
+ issue_row.created_by_user_id as "createdByUserId",
574
+ issue_row.identifier as identifier,
575
+ issue_row.updated_at as "updatedAt",
576
+ exists (
577
+ select 1
578
+ from ${issueFollows} follow_row
579
+ where follow_row.org_id = ${orgId}
580
+ and follow_row.user_id = ${userId}
581
+ and follow_row.issue_id = issue_row.id
582
+ ) as followed,
583
+ (issue_row.assignee_user_id = ${userId}) as assigned,
584
+ greatest(
585
+ issue_row.updated_at,
586
+ coalesce(latest_external_comment.created_at, issue_row.updated_at),
587
+ coalesce(latest_activity.created_at, issue_row.updated_at)
588
+ ) as "latestActivityAt",
589
+ latest_activity.id as "latestActivityId",
590
+ latest_activity.action as "latestActivityAction",
591
+ latest_activity.actor_type as "latestActivityActorType",
592
+ latest_activity.actor_id as "latestActivityActorId",
593
+ latest_activity.details as "latestActivityDetails",
594
+ latest_activity.created_at as "latestActivityCreatedAt",
595
+ latest_activity.run_id as "latestActivityRunId",
596
+ case
597
+ when latest_external_comment.created_at is not null
598
+ and (latest_external_activity.created_at is null or latest_external_comment.created_at >= latest_external_activity.created_at)
599
+ then latest_external_comment.created_at
600
+ when latest_external_activity.created_at is not null
601
+ then latest_external_activity.created_at
602
+ when latest_activity.id is null
603
+ and (
604
+ latest_suppressed_activity.created_at is null
605
+ or latest_suppressed_activity.created_at < issue_row.updated_at - interval '5 seconds'
606
+ )
607
+ and (
608
+ issue_row.assignee_user_id = ${userId}
609
+ or (issue_row.reviewer_user_id = ${userId} and issue_row.status = 'in_review')
610
+ )
611
+ then issue_row.updated_at
612
+ else null
613
+ end as "attentionActivityAt",
614
+ latest_external_comment.body as "latestExternalCommentBody",
615
+ latest_external_comment.created_at as "latestExternalCommentCreatedAt",
616
+ latest_external_activity.id as "latestExternalActivityId",
617
+ latest_external_activity.action as "latestExternalActivityAction",
618
+ latest_external_activity.actor_type as "latestExternalActivityActorType",
619
+ latest_external_activity.actor_id as "latestExternalActivityActorId",
620
+ latest_external_activity.details as "latestExternalActivityDetails",
621
+ latest_external_activity.created_at as "latestExternalActivityCreatedAt",
622
+ latest_external_activity.run_id as "latestExternalActivityRunId"
623
+ from tracked_issue_ids
624
+ inner join ${issues} issue_row on issue_row.id = tracked_issue_ids.id
625
+ left join lateral (
626
+ select
627
+ comment_row.body,
628
+ comment_row.created_at
629
+ from ${issueComments} comment_row
630
+ where comment_row.org_id = ${orgId}
631
+ and comment_row.issue_id = issue_row.id
632
+ and (comment_row.author_user_id is null or comment_row.author_user_id <> ${userId})
633
+ order by comment_row.created_at desc, comment_row.id desc
634
+ limit 1
635
+ ) latest_external_comment on true
636
+ left join lateral (
637
+ select
638
+ activity_row.id,
639
+ activity_row.action,
640
+ activity_row.actor_type,
641
+ activity_row.actor_id,
642
+ activity_row.details,
643
+ activity_row.created_at,
644
+ activity_row.run_id
645
+ from ${activityLog} activity_row
646
+ where activity_row.org_id = ${orgId}
647
+ and activity_row.entity_type = 'issue'
648
+ and activity_row.entity_id = issue_row.id::text
649
+ and activity_row.action in (${issueActionSqlList})
650
+ and not ${descriptionOnlyActivity}
651
+ order by activity_row.created_at desc, activity_row.id desc
652
+ limit 1
653
+ ) latest_activity on true
654
+ left join lateral (
655
+ select
656
+ external_activity_row.id,
657
+ external_activity_row.action,
658
+ external_activity_row.actor_type,
659
+ external_activity_row.actor_id,
660
+ external_activity_row.details,
661
+ external_activity_row.created_at,
662
+ external_activity_row.run_id
663
+ from ${activityLog} external_activity_row
664
+ where external_activity_row.org_id = ${orgId}
665
+ and external_activity_row.entity_type = 'issue'
666
+ and external_activity_row.entity_id = issue_row.id::text
667
+ and external_activity_row.action in (${issueActionSqlList})
668
+ and not ${externalDescriptionOnlyActivity}
669
+ and (external_activity_row.actor_type <> 'user' or external_activity_row.actor_id <> ${userId})
670
+ order by external_activity_row.created_at desc, external_activity_row.id desc
671
+ limit 1
672
+ ) latest_external_activity on true
673
+ left join lateral (
674
+ select suppressed_activity_row.created_at
675
+ from ${activityLog} suppressed_activity_row
676
+ where suppressed_activity_row.org_id = ${orgId}
677
+ and suppressed_activity_row.entity_type = 'issue'
678
+ and suppressed_activity_row.entity_id = issue_row.id::text
679
+ and suppressed_activity_row.action in (${issueActionSqlList})
680
+ and ${issueDescriptionOnlyActivitySql("suppressed_activity_row")}
681
+ order by suppressed_activity_row.created_at desc, suppressed_activity_row.id desc
682
+ limit 1
683
+ ) latest_suppressed_activity on true
684
+ )
685
+ select *
686
+ from issue_entries
687
+ ${tail}
688
+ `;
689
+ }
690
+ async function loadLatestIssueCommentsForDisplay(orgId, issueIds) {
691
+ if (issueIds.length === 0)
692
+ return [];
693
+ return (await db
694
+ .selectDistinctOn([issueComments.issueId], {
695
+ id: issueComments.id,
696
+ issueId: issueComments.issueId,
697
+ body: issueComments.body,
698
+ authorAgentId: issueComments.authorAgentId,
699
+ authorUserId: issueComments.authorUserId,
700
+ authorAgentName: agents.name,
701
+ authorUserName: authUsers.name,
702
+ createdAt: issueComments.createdAt,
703
+ })
704
+ .from(issueComments)
705
+ .leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
706
+ .leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
707
+ .where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds)))
708
+ .orderBy(issueComments.issueId, desc(issueComments.createdAt), desc(issueComments.id)));
709
+ }
710
+ function issueThreadEntryFromRow(row, userId) {
711
+ const updatedAt = normalizeDate(row.updatedAt) ?? new Date(row.updatedAt);
712
+ const latestActivityAt = normalizeDate(row.latestActivityAt) ?? updatedAt;
713
+ const latestActivityCreatedAt = normalizeDate(row.latestActivityCreatedAt);
714
+ const latestExternalActivityCreatedAt = normalizeDate(row.latestExternalActivityCreatedAt);
715
+ const issue = {
716
+ id: row.id,
717
+ title: row.title,
718
+ status: row.status,
719
+ priority: row.priority,
720
+ assigneeUserId: row.assigneeUserId,
721
+ reviewerUserId: row.reviewerUserId,
722
+ createdByUserId: row.createdByUserId,
723
+ identifier: row.identifier,
724
+ updatedAt,
725
+ followed: row.followed,
726
+ assigned: row.assigned,
727
+ };
728
+ const latestActivity = row.latestActivityId && row.latestActivityAction && row.latestActivityActorType && row.latestActivityActorId && latestActivityCreatedAt
729
+ ? {
730
+ id: row.latestActivityId,
731
+ action: row.latestActivityAction,
732
+ entityId: row.id,
733
+ actorType: row.latestActivityActorType,
734
+ actorId: row.latestActivityActorId,
735
+ details: row.latestActivityDetails,
736
+ createdAt: latestActivityCreatedAt,
737
+ runId: row.latestActivityRunId,
738
+ }
739
+ : null;
740
+ const latestExternalActivity = row.latestExternalActivityId &&
741
+ row.latestExternalActivityAction &&
742
+ row.latestExternalActivityActorType &&
743
+ row.latestExternalActivityActorId &&
744
+ latestExternalActivityCreatedAt
745
+ ? {
746
+ id: row.latestExternalActivityId,
747
+ action: row.latestExternalActivityAction,
748
+ entityId: row.id,
749
+ actorType: row.latestExternalActivityActorType,
750
+ actorId: row.latestExternalActivityActorId,
751
+ details: row.latestExternalActivityDetails,
752
+ createdAt: latestExternalActivityCreatedAt,
753
+ runId: row.latestExternalActivityRunId,
754
+ }
755
+ : null;
756
+ const latestExternalCommentAt = normalizeDate(row.latestExternalCommentCreatedAt);
757
+ const attentionActivityAt = normalizeDate(row.attentionActivityAt);
758
+ const latestExternalActivityAt = normalizeDate(latestExternalActivity?.createdAt ?? null);
759
+ const attentionPreview = latestExternalCommentAt &&
760
+ (!latestExternalActivityAt || latestExternalCommentAt.getTime() >= latestExternalActivityAt.getTime())
761
+ ? truncate(row.latestExternalCommentBody)
762
+ : latestExternalActivity
763
+ ? summarizeIssueActivity(latestExternalActivity, issue)
764
+ : null;
765
+ const fallbackPreview = attentionPreview
766
+ ?? (attentionActivityAt
767
+ ? issueBodyFromSnapshot(issue, null, row.followed, row.createdByUserId === userId, row.assigneeUserId === userId, row.reviewerUserId === userId && row.status === "in_review")
768
+ : null);
769
+ return {
770
+ issue,
771
+ latestActivityAt,
772
+ latestActivity,
773
+ attentionActivityAt,
774
+ attentionPreview: attentionActivityAt ? issueThreadPreview(issue, fallbackPreview) : null,
775
+ };
776
+ }
777
+ async function loadIssueThreadStats(orgId, userId, lastReadAt) {
778
+ const lastReadAtIso = lastReadAt?.toISOString() ?? null;
779
+ const rows = (await db.execute(sql `
780
+ select
781
+ count(*)::int as "itemCount",
782
+ count(*) filter (
783
+ where "attentionActivityAt" is not null
784
+ and (${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz)
785
+ )::int as "unreadCount",
786
+ max("attentionActivityAt") filter (
787
+ where ${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz
788
+ ) as "latestActivityAt"
789
+ from (${issueEntryRowsQuery(orgId, userId)}) issue_entry_stats
790
+ `));
791
+ const row = rows[0];
792
+ return row
793
+ ? {
794
+ itemCount: Number(row.itemCount),
795
+ unreadCount: Number(row.unreadCount),
796
+ latestActivityAt: normalizeDate(row.latestActivityAt),
797
+ }
798
+ : { itemCount: 0, unreadCount: 0, latestActivityAt: null };
799
+ }
800
+ async function loadLatestUnreadIssueEntry(orgId, userId, lastReadAt) {
801
+ const lastReadAtIso = lastReadAt?.toISOString() ?? null;
802
+ const rows = (await db.execute(issueEntryRowsQuery(orgId, userId, sql `
803
+ where "attentionActivityAt" is not null
804
+ and (${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz)
805
+ order by "attentionActivityAt" desc, id asc
806
+ limit 1
807
+ `)));
808
+ return rows[0] ? issueThreadEntryFromRow(rows[0], userId) : null;
809
+ }
810
+ async function loadIssueDetailEntries(orgId, userId, limit, cursor) {
811
+ const cursorActivityAt = cursor ? new Date(cursor.activityAt).toISOString() : null;
812
+ const rows = (await db.execute(issueEntryRowsQuery(orgId, userId, sql `
813
+ ${cursor
814
+ ? sql `
815
+ where (
816
+ "latestActivityAt" < ${cursorActivityAt}::timestamptz
817
+ or ("latestActivityAt" = ${cursorActivityAt}::timestamptz and id > ${cursor.issueId})
818
+ )
819
+ `
820
+ : sql ``}
821
+ order by "latestActivityAt" desc, id asc
822
+ limit ${limit + 1}
823
+ `)));
824
+ return rows.map((row) => issueThreadEntryFromRow(row, userId));
544
825
  }
545
826
  async function loadIssueData(orgId, userId, threadStates, options) {
546
827
  const lastReadAtPromise = lastReadAtForThread(db, orgId, userId, "issues", threadStates);
547
- const issuesUniverse = await loadIssueUniverse(orgId, userId);
548
- const issueIds = issuesUniverse.map((row) => row.id);
549
828
  const lastReadAt = await lastReadAtPromise;
550
- const [commentRows, activityRows] = await Promise.all([
551
- issueIds.length === 0
552
- ? Promise.resolve([])
553
- : options.includeDetail
554
- ? db
555
- .select({
556
- id: issueComments.id,
557
- issueId: issueComments.issueId,
558
- body: issueComments.body,
559
- authorAgentId: issueComments.authorAgentId,
560
- authorUserId: issueComments.authorUserId,
561
- authorAgentName: agents.name,
562
- authorUserName: authUsers.name,
563
- createdAt: issueComments.createdAt,
564
- })
565
- .from(issueComments)
566
- .leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
567
- .leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
568
- .where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds)))
569
- .orderBy(desc(issueComments.createdAt))
570
- : db
571
- .select({
572
- id: issueComments.id,
573
- issueId: issueComments.issueId,
574
- body: issueComments.body,
575
- authorAgentId: issueComments.authorAgentId,
576
- authorUserId: issueComments.authorUserId,
577
- authorAgentName: sql `null`,
578
- authorUserName: sql `null`,
579
- createdAt: issueComments.createdAt,
580
- })
581
- .from(issueComments)
582
- .where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds), sql `(${issueComments.authorUserId} is null or ${issueComments.authorUserId} <> ${userId})`))
583
- .orderBy(desc(issueComments.createdAt)),
584
- issueIds.length === 0
585
- ? Promise.resolve([])
586
- : db
587
- .select({
588
- id: activityLog.id,
589
- action: activityLog.action,
590
- entityId: activityLog.entityId,
591
- actorType: activityLog.actorType,
592
- actorId: activityLog.actorId,
593
- details: activityLog.details,
594
- createdAt: activityLog.createdAt,
595
- runId: activityLog.runId,
596
- })
597
- .from(activityLog)
598
- .where(and(eq(activityLog.orgId, orgId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIds), inArray(activityLog.action, ISSUE_ACTIVITY_ACTIONS)))
599
- .orderBy(desc(activityLog.createdAt)),
600
- ]);
601
- const latestCommentByIssue = new Map();
602
- const latestExternalCommentByIssue = new Map();
603
- for (const row of commentRows) {
604
- if (options.includeDetail && !latestCommentByIssue.has(row.issueId)) {
605
- latestCommentByIssue.set(row.issueId, row);
606
- }
607
- if (!isSelfAuthoredComment(row, userId) && !latestExternalCommentByIssue.has(row.issueId)) {
608
- latestExternalCommentByIssue.set(row.issueId, row);
609
- }
829
+ const detailLimit = normalizeIssueThreadLimit(options.limit);
830
+ const decodedCursor = decodeIssueThreadCursor(options.cursor);
831
+ if (options.cursor && !decodedCursor) {
832
+ throw conflict("Messenger issues cursor is invalid or expired");
610
833
  }
611
- const latestActivityByIssue = new Map();
612
- const latestExternalActivityByIssue = new Map();
613
- const latestSuppressedActivityByIssue = new Map();
614
- for (const row of activityRows) {
615
- if (!shouldNotifyIssueActivity(row)) {
616
- if (!latestSuppressedActivityByIssue.has(row.entityId)) {
617
- latestSuppressedActivityByIssue.set(row.entityId, row);
618
- }
619
- continue;
620
- }
621
- if (!latestActivityByIssue.has(row.entityId)) {
622
- latestActivityByIssue.set(row.entityId, row);
623
- }
624
- if (!isSelfAuthoredActivity(row, userId) && !latestExternalActivityByIssue.has(row.entityId)) {
625
- latestExternalActivityByIssue.set(row.entityId, row);
626
- }
834
+ const [stats, latestAttentionEntry, detailEntries] = await Promise.all([
835
+ loadIssueThreadStats(orgId, userId, lastReadAt),
836
+ loadLatestUnreadIssueEntry(orgId, userId, lastReadAt),
837
+ options.includeDetail
838
+ ? loadIssueDetailEntries(orgId, userId, detailLimit, decodedCursor)
839
+ : Promise.resolve([]),
840
+ ]);
841
+ const hasMoreDetailEntries = options.includeDetail && detailEntries.length > detailLimit;
842
+ const pageEntries = hasMoreDetailEntries ? detailEntries.slice(0, detailLimit) : detailEntries;
843
+ const cursorEntry = hasMoreDetailEntries ? pageEntries.at(-1) ?? null : null;
844
+ const latestDisplayCommentRows = await loadLatestIssueCommentsForDisplay(orgId, pageEntries.map((entry) => entry.issue.id));
845
+ const latestDisplayCommentByIssue = new Map();
846
+ for (const row of latestDisplayCommentRows) {
847
+ latestDisplayCommentByIssue.set(row.issueId, row);
627
848
  }
628
- const unsortedEntries = issuesUniverse.map((issue) => {
629
- const latestComment = latestCommentByIssue.get(issue.id) ?? null;
630
- const latestExternalComment = latestExternalCommentByIssue.get(issue.id) ?? null;
631
- const latestActivity = latestActivityByIssue.get(issue.id) ?? null;
632
- const latestVisibleComment = latestExternalComment;
633
- const latestVisibleCommentAt = normalizeDate(latestVisibleComment?.createdAt ?? null);
634
- const latestEventAt = maxDate(latestVisibleCommentAt, latestActivity?.createdAt);
635
- const latestActivityAt = maxDate(issue.updatedAt, latestEventAt);
636
- const latestSourceIsComment = latestVisibleCommentAt &&
637
- (!latestActivity?.createdAt || latestVisibleCommentAt.getTime() >= new Date(latestActivity.createdAt).getTime());
638
- const latestPreview = latestSourceIsComment
639
- ? truncate(latestVisibleComment?.body)
640
- : latestActivity
641
- ? summarizeIssueActivity(latestActivity, issue)
849
+ const chronologicalItems = pageEntries
850
+ .sort(compareIssueThreadEntriesChronological)
851
+ .map((entry) => {
852
+ const latestDisplayComment = latestDisplayCommentByIssue.get(entry.issue.id) ?? null;
853
+ const latestDisplayCommentAt = normalizeDate(latestDisplayComment?.createdAt ?? null);
854
+ const latestSourceIsComment = Boolean(latestDisplayCommentAt &&
855
+ (!entry.latestActivity?.createdAt || latestDisplayCommentAt.getTime() >= new Date(entry.latestActivity.createdAt).getTime()));
856
+ const sourceComment = latestSourceIsComment ? latestDisplayComment : null;
857
+ const latestPreview = sourceComment
858
+ ? truncate(sourceComment.body)
859
+ : entry.latestActivity
860
+ ? summarizeIssueActivity(entry.latestActivity, entry.issue)
642
861
  : null;
643
- const statusChangeActivity = latestSourceIsComment
644
- ? (issueStatusActivityMatchesSourceComment(latestActivity, latestVisibleComment) ? latestActivity : null)
645
- : latestActivity;
646
- const latestExternalActivity = latestExternalActivityByIssue.get(issue.id) ?? null;
647
- const latestExternalCommentAt = normalizeDate(latestExternalComment?.createdAt ?? null);
648
- const latestSuppressedActivityAt = normalizeDate(latestSuppressedActivityByIssue.get(issue.id)?.createdAt ?? null);
649
- const issueUpdatedAt = normalizeDate(issue.updatedAt);
650
- const suppressedActivityMatchesIssueUpdate = Boolean(latestSuppressedActivityAt &&
651
- issueUpdatedAt &&
652
- latestSuppressedActivityAt.getTime() >= issueUpdatedAt.getTime() - 5_000);
653
- const fallbackAssignedActivityAt = issue.assigneeUserId === userId && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
654
- ? issueUpdatedAt
655
- : null;
656
- const fallbackReviewerActivityAt = issue.reviewerUserId === userId && issue.status === "in_review" && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
657
- ? issueUpdatedAt
658
- : null;
659
- const attentionActivityAt = maxDate(latestExternalCommentAt, latestExternalActivity?.createdAt, fallbackAssignedActivityAt, fallbackReviewerActivityAt);
660
- const attentionPreview = latestExternalCommentAt &&
661
- (!latestExternalActivity?.createdAt || latestExternalCommentAt.getTime() >= new Date(latestExternalActivity.createdAt).getTime())
662
- ? truncate(latestExternalComment?.body)
663
- : latestExternalActivity
664
- ? summarizeIssueActivity(latestExternalActivity, issue)
665
- : fallbackAssignedActivityAt || fallbackReviewerActivityAt
666
- ? issueBodyFromSnapshot(issue, null, issue.followed, issue.createdByUserId === userId, issue.assigneeUserId === userId, issue.reviewerUserId === userId && issue.status === "in_review")
667
- : null;
668
- const summaryPreview = attentionActivityAt ? issueThreadPreview(issue, attentionPreview) : null;
669
- return {
670
- issue,
671
- item: options.includeDetail
672
- ? issueCard(issue, userId, issue.followed, latestPreview, latestActivityAt ?? issue.updatedAt, latestSourceIsComment ? latestVisibleComment : null, statusChangeActivity)
673
- : null,
674
- attentionActivityAt,
675
- attentionPreview: summaryPreview,
676
- };
677
- });
678
- const latestFirstEntries = [...unsortedEntries].sort((a, b) => {
679
- const aTime = a.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
680
- const bTime = b.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
681
- if (aTime !== bTime)
682
- return bTime - aTime;
683
- return issueDisplayLabel(a.issue).localeCompare(issueDisplayLabel(b.issue));
862
+ const statusChangeActivity = sourceComment
863
+ ? (issueStatusActivityMatchesSourceComment(entry.latestActivity, sourceComment) ? entry.latestActivity : null)
864
+ : entry.latestActivity;
865
+ return issueCard(entry.issue, userId, entry.issue.followed, latestPreview, entry.latestActivityAt, sourceComment, statusChangeActivity);
684
866
  });
685
- const chronologicalItems = options.includeDetail
686
- ? unsortedEntries
687
- .flatMap((entry) => entry.item ? [entry.item] : [])
688
- .sort(compareChronologicalActivity)
689
- : [];
690
- const latestAttentionEntry = latestFirstEntries.find((entry) => entry.attentionActivityAt);
691
- const latestActivityAt = latestAttentionEntry?.attentionActivityAt ?? null;
692
- const unreadCount = unsortedEntries.filter((entry) => {
693
- const itemActivity = entry.attentionActivityAt;
694
- if (!itemActivity)
695
- return false;
696
- if (!lastReadAt)
697
- return true;
698
- return itemActivity.getTime() > lastReadAt.getTime();
699
- }).length;
700
867
  const data = {
701
- summary: issueSummary(issuesUniverse.length, latestActivityAt, unreadCount, lastReadAt, latestAttentionEntry?.attentionPreview ?? null),
702
- itemCount: issuesUniverse.length,
868
+ summary: issueSummary(stats.itemCount, stats.latestActivityAt, stats.unreadCount, lastReadAt, latestAttentionEntry?.attentionPreview ?? null),
869
+ itemCount: stats.itemCount,
703
870
  };
704
871
  if (options.includeDetail) {
705
872
  data.detail = {
706
873
  threadKey: "issues",
707
874
  kind: "issues",
708
875
  title: "Issues",
709
- subtitle: `${issuesUniverse.length} tracked issue${issuesUniverse.length === 1 ? "" : "s"}`,
876
+ subtitle: `${stats.itemCount} tracked issue${stats.itemCount === 1 ? "" : "s"}`,
710
877
  preview: latestAttentionEntry?.attentionPreview ?? null,
711
- latestActivityAt,
878
+ latestActivityAt: stats.latestActivityAt,
712
879
  lastReadAt,
713
- unreadCount,
714
- needsAttention: unreadCount > 0,
880
+ unreadCount: stats.unreadCount,
881
+ needsAttention: stats.unreadCount > 0,
715
882
  isPinned: false,
716
883
  href: "/messenger/issues",
717
884
  description: "Followed issues, issues I created, issues assigned to me, and issues ready for my review",
718
885
  items: chronologicalItems,
886
+ pageInfo: {
887
+ limit: detailLimit,
888
+ nextCursor: cursorEntry ? encodeIssueThreadCursor(cursorEntry) : null,
889
+ hasMore: hasMoreDetailEntries,
890
+ },
719
891
  };
720
892
  }
721
893
  return data;
722
894
  }
723
- async function loadIssueSummaryData(orgId, userId, threadStates) {
724
- const data = await loadIssueData(orgId, userId, threadStates, { includeDetail: true });
895
+ async function loadIssueSummaryData(orgId, userId, threadStates, options = {}) {
896
+ const data = await loadIssueData(orgId, userId, threadStates, { includeDetail: true, ...options });
725
897
  return {
726
898
  summary: data.summary,
727
899
  detail: data.detail,
@@ -1106,8 +1278,8 @@ export function messengerService(db) {
1106
1278
  });
1107
1279
  return threadSummaries;
1108
1280
  }
1109
- async function getIssuesThread(orgId, userId) {
1110
- return loadIssueSummaryData(orgId, userId);
1281
+ async function getIssuesThread(orgId, userId, options = {}) {
1282
+ return loadIssueSummaryData(orgId, userId, undefined, options);
1111
1283
  }
1112
1284
  async function getApprovalsThread(orgId, userId) {
1113
1285
  return loadApprovalSummaryData(orgId, userId);