@mastra/factory 0.10.2-alpha.2 → 0.11.0
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.
- package/CHANGELOG.md +60 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +18 -2
- package/dist/factory.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +101 -0
- package/dist/routes/attention-providers.d.ts.map +1 -0
- package/dist/routes/attention-providers.js +384 -0
- package/dist/routes/attention-providers.js.map +1 -0
- package/dist/routes/attention.d.ts +11 -9
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js +143 -193
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/surface.d.ts +2 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +3 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts +3 -0
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +3 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +3 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +9 -1
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +2 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +10 -2
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/storage/domains/audit/domain.d.ts.map +1 -1
- package/dist/storage/domains/audit/domain.js +2 -5
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.d.ts +4 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -1
- package/dist/storage/domains/channel-identity/base.js +15 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/actor.d.ts +51 -0
- package/dist/storage/domains/comments/actor.d.ts.map +1 -0
- package/dist/storage/domains/comments/actor.js +38 -0
- package/dist/storage/domains/comments/actor.js.map +1 -0
- package/dist/storage/domains/comments/base.d.ts +230 -0
- package/dist/storage/domains/comments/base.d.ts.map +1 -0
- package/dist/storage/domains/comments/base.js +490 -0
- package/dist/storage/domains/comments/base.js.map +1 -0
- package/dist/storage/domains/comments/domain.d.ts +125 -0
- package/dist/storage/domains/comments/domain.d.ts.map +1 -0
- package/dist/storage/domains/comments/domain.js +270 -0
- package/dist/storage/domains/comments/domain.js.map +1 -0
- package/dist/storage/domains/comments/feed-context.d.ts +20 -0
- package/dist/storage/domains/comments/feed-context.d.ts.map +1 -0
- package/dist/storage/domains/comments/feed-context.js +74 -0
- package/dist/storage/domains/comments/feed-context.js.map +1 -0
- package/dist/storage/domains/comments/feed-sync.d.ts +38 -0
- package/dist/storage/domains/comments/feed-sync.d.ts.map +1 -0
- package/dist/storage/domains/comments/feed-sync.js +1 -0
- package/dist/storage/domains/comments/parse.d.ts +22 -0
- package/dist/storage/domains/comments/parse.d.ts.map +1 -0
- package/dist/storage/domains/comments/parse.js +90 -0
- package/dist/storage/domains/comments/parse.js.map +1 -0
- package/dist/storage/domains/comments/routes.d.ts +22 -0
- package/dist/storage/domains/comments/routes.d.ts.map +1 -0
- package/dist/storage/domains/comments/routes.js +248 -0
- package/dist/storage/domains/comments/routes.js.map +1 -0
- package/dist/storage/domains/comments/schema.d.ts +4 -0
- package/dist/storage/domains/comments/schema.d.ts.map +1 -0
- package/dist/storage/domains/comments/schema.js +135 -0
- package/dist/storage/domains/comments/schema.js.map +1 -0
- package/dist/storage/domains/comments/wire.d.ts +40 -0
- package/dist/storage/domains/comments/wire.d.ts.map +1 -0
- package/dist/storage/domains/comments/wire.js +37 -0
- package/dist/storage/domains/comments/wire.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +14 -10
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +109 -22
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/factory-skills/factory-rereview/SKILL.md +20 -12
- package/factory-skills/factory-review/SKILL.md +17 -12
- package/package.json +7 -7
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA } from "./schema.js";
|
|
2
|
+
import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
|
|
3
|
+
//#region src/storage/domains/comments/base.ts
|
|
4
|
+
/**
|
|
5
|
+
* Work-item comments domain — one message list per work item, materialized
|
|
6
|
+
* from every source. Local creates and platform mirrors (Slack, Linear,
|
|
7
|
+
* GitHub) share one idempotency story: `external_source` json + derived
|
|
8
|
+
* `source_key` under a partial unique index, a local retry token being just
|
|
9
|
+
* another source (`local:comment:<uuid>`).
|
|
10
|
+
*
|
|
11
|
+
* Ordering is caller-settable `occurred_at`, never insert time — ingest
|
|
12
|
+
* backdates to the platform timestamp and retries arrive out of order.
|
|
13
|
+
* Deletes are soft: the tombstone holds the ordering and its kept
|
|
14
|
+
* `source_key` stops a redelivery from resurrecting the row.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The refresh reads its snapshot outside the write transaction, so two of them
|
|
18
|
+
* can interleave and the loser can write last. This is the one rule that keeps
|
|
19
|
+
* that from undoing a newer refresh: feed activity never moves backwards, and
|
|
20
|
+
* on the same stamp the fuller count wins.
|
|
21
|
+
*
|
|
22
|
+
* Known ceiling: a soft delete sharing a millisecond with a create leaves the
|
|
23
|
+
* count one high until the next feed mutation recounts. Closing that needs the
|
|
24
|
+
* aggregate read inside the write transaction, which the ops layer cannot do
|
|
25
|
+
* without checking out a second pool connection per open transaction.
|
|
26
|
+
*/
|
|
27
|
+
function supersedesFeedActivity(next, stored) {
|
|
28
|
+
const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();
|
|
29
|
+
return drift > 0 || drift === 0 && next.commentCount >= stored.commentCount;
|
|
30
|
+
}
|
|
31
|
+
const MAX_COMMENT_BODY_LENGTH = 16e3;
|
|
32
|
+
const MAX_COMMENT_QUOTE_LENGTH = 500;
|
|
33
|
+
const MAX_COMMENT_MENTIONS = 20;
|
|
34
|
+
/** The one body policy: HTTP parsing and the service both reject on it. */
|
|
35
|
+
function commentBodyError(body) {
|
|
36
|
+
if (!body.trim()) return "Comment body must not be empty.";
|
|
37
|
+
if (body.length > 16e3) return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;
|
|
38
|
+
}
|
|
39
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
40
|
+
const MAX_PAGE_SIZE = 100;
|
|
41
|
+
function clampCommentLimit(limit) {
|
|
42
|
+
return Math.min(Math.max(typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE, 1), MAX_PAGE_SIZE);
|
|
43
|
+
}
|
|
44
|
+
function encodeCommentCursor(row) {
|
|
45
|
+
return `${row.occurredAt.toISOString()}_${row.id}`;
|
|
46
|
+
}
|
|
47
|
+
function decodeCommentCursor(cursor) {
|
|
48
|
+
const sep = cursor.lastIndexOf("_");
|
|
49
|
+
if (sep <= 0) return void 0;
|
|
50
|
+
const occurredAt = new Date(cursor.slice(0, sep));
|
|
51
|
+
const id = cursor.slice(sep + 1);
|
|
52
|
+
if (Number.isNaN(occurredAt.getTime()) || !id) return void 0;
|
|
53
|
+
return {
|
|
54
|
+
occurredAt,
|
|
55
|
+
id
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function commentSourceKey(input) {
|
|
59
|
+
if (input.externalSource) {
|
|
60
|
+
const source = input.externalSource;
|
|
61
|
+
return `${source.integrationId}:${source.type}:${source.externalId}`;
|
|
62
|
+
}
|
|
63
|
+
if (input.clientToken) return `local:comment:${input.clientToken}`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** A `clientToken` replay that resolved to a different work item or author. */
|
|
67
|
+
var CommentTokenConflictError = class extends Error {
|
|
68
|
+
constructor() {
|
|
69
|
+
super("Client token already used by a different comment.");
|
|
70
|
+
this.name = "CommentTokenConflictError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function toComment(row) {
|
|
74
|
+
return {
|
|
75
|
+
id: row.id,
|
|
76
|
+
orgId: row.org_id,
|
|
77
|
+
factoryProjectId: row.factory_project_id,
|
|
78
|
+
workItemId: row.work_item_id,
|
|
79
|
+
kind: row.kind,
|
|
80
|
+
body: row.body,
|
|
81
|
+
bodyFormat: row.body_format,
|
|
82
|
+
author: {
|
|
83
|
+
kind: row.author_kind,
|
|
84
|
+
id: row.author_id,
|
|
85
|
+
...row.author_display_name ? { displayName: row.author_display_name } : {},
|
|
86
|
+
...row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {},
|
|
87
|
+
...row.author_external ? { external: row.author_external } : {}
|
|
88
|
+
},
|
|
89
|
+
replyTo: row.reply_to_comment_id ? {
|
|
90
|
+
commentId: row.reply_to_comment_id,
|
|
91
|
+
...row.reply_quote ? { quote: row.reply_quote } : {},
|
|
92
|
+
...row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {},
|
|
93
|
+
...row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}
|
|
94
|
+
} : null,
|
|
95
|
+
mentions: row.mentions,
|
|
96
|
+
externalSource: row.external_source,
|
|
97
|
+
sourceKey: row.source_key,
|
|
98
|
+
occurredAt: row.occurred_at,
|
|
99
|
+
editedAt: row.edited_at,
|
|
100
|
+
deletedAt: row.deleted_at,
|
|
101
|
+
deletedBy: row.deleted_by,
|
|
102
|
+
revision: row.revision,
|
|
103
|
+
createdAt: row.created_at,
|
|
104
|
+
updatedAt: row.updated_at
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function toMention(row) {
|
|
108
|
+
return {
|
|
109
|
+
id: row.id,
|
|
110
|
+
commentId: row.comment_id,
|
|
111
|
+
mentionedKind: row.mentioned_kind,
|
|
112
|
+
mentionedId: row.mentioned_id,
|
|
113
|
+
authorId: row.author_id,
|
|
114
|
+
orgId: row.org_id,
|
|
115
|
+
factoryProjectId: row.factory_project_id,
|
|
116
|
+
workItemId: row.work_item_id,
|
|
117
|
+
occurredAt: row.occurred_at
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function toMentionRef(row) {
|
|
121
|
+
return {
|
|
122
|
+
kind: row.mentionedKind,
|
|
123
|
+
id: row.mentionedId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function mentionKey(mention) {
|
|
127
|
+
return `${mention.kind}\0${mention.id}`;
|
|
128
|
+
}
|
|
129
|
+
function dedupeMentions(mentions) {
|
|
130
|
+
if (!mentions?.length) return [];
|
|
131
|
+
return [...new Map(mentions.map((mention) => [mentionKey(mention), mention])).values()];
|
|
132
|
+
}
|
|
133
|
+
var WorkItemCommentsStorage = class extends FactoryStorageDomain {
|
|
134
|
+
constructor() {
|
|
135
|
+
super("work-item-comments");
|
|
136
|
+
}
|
|
137
|
+
async init() {
|
|
138
|
+
await this.ensureCollections([WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA]);
|
|
139
|
+
}
|
|
140
|
+
async dangerouslyClearAll() {
|
|
141
|
+
await this.ops.deleteMany("work_item_comment_mentions", {});
|
|
142
|
+
await this.ops.deleteMany("work_item_comments", {});
|
|
143
|
+
}
|
|
144
|
+
async create(input) {
|
|
145
|
+
const now = /* @__PURE__ */ new Date();
|
|
146
|
+
const occurredAt = input.occurredAt ?? now;
|
|
147
|
+
const sourceKey = commentSourceKey(input);
|
|
148
|
+
const author = input.author;
|
|
149
|
+
const row = {
|
|
150
|
+
org_id: input.orgId,
|
|
151
|
+
factory_project_id: input.factoryProjectId,
|
|
152
|
+
work_item_id: input.workItemId,
|
|
153
|
+
kind: "comment",
|
|
154
|
+
body: input.body,
|
|
155
|
+
body_format: input.bodyFormat ?? "markdown",
|
|
156
|
+
author_kind: author.kind,
|
|
157
|
+
author_id: author.id,
|
|
158
|
+
author_display_name: author.displayName ?? null,
|
|
159
|
+
author_avatar_url: author.avatarUrl ?? null,
|
|
160
|
+
author_external: author.external ?? null,
|
|
161
|
+
reply_to_comment_id: input.replyTo?.commentId ?? null,
|
|
162
|
+
reply_quote: input.replyTo?.quote ?? null,
|
|
163
|
+
reply_to_author_id: input.replyTo?.authorId ?? null,
|
|
164
|
+
reply_to_author_name: input.replyTo?.authorName ?? null,
|
|
165
|
+
mentions: dedupeMentions(input.mentions),
|
|
166
|
+
external_source: input.externalSource ?? null,
|
|
167
|
+
source_key: sourceKey,
|
|
168
|
+
occurred_at: occurredAt,
|
|
169
|
+
edited_at: null,
|
|
170
|
+
deleted_at: null,
|
|
171
|
+
deleted_by: null,
|
|
172
|
+
revision: 1,
|
|
173
|
+
created_at: now,
|
|
174
|
+
updated_at: now
|
|
175
|
+
};
|
|
176
|
+
if (sourceKey) {
|
|
177
|
+
const sourceWhere = {
|
|
178
|
+
org_id: input.orgId,
|
|
179
|
+
factory_project_id: input.factoryProjectId,
|
|
180
|
+
source_key: sourceKey
|
|
181
|
+
};
|
|
182
|
+
const recover = (found) => {
|
|
183
|
+
if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) throw new CommentTokenConflictError();
|
|
184
|
+
return toComment(found);
|
|
185
|
+
};
|
|
186
|
+
const existing = await this.ops.findOne("work_item_comments", sourceWhere);
|
|
187
|
+
if (existing) return recover(existing);
|
|
188
|
+
try {
|
|
189
|
+
return await this.#insertWithMentions(row);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (!(error instanceof UniqueViolationError)) throw error;
|
|
192
|
+
const raced = await this.ops.findOne("work_item_comments", sourceWhere);
|
|
193
|
+
if (!raced) throw error;
|
|
194
|
+
return recover(raced);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return this.#insertWithMentions(row);
|
|
198
|
+
}
|
|
199
|
+
async #insertWithMentions(row) {
|
|
200
|
+
const comment = toComment(await this.ops.insertOne("work_item_comments", row));
|
|
201
|
+
await this.#writeMentionRows(comment, comment.mentions);
|
|
202
|
+
return comment;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Idempotent counter refresh on the parent work item: both columns are read
|
|
206
|
+
* back off the comments (never incremented or stamped with the wall clock —
|
|
207
|
+
* replays and races double an increment, and a replayed create would move a
|
|
208
|
+
* work item in the feed without adding a comment; a read-back can't drift).
|
|
209
|
+
* Read BEFORE `updateAtomic`: its mutator runs inside an open transaction
|
|
210
|
+
* holding a pool connection, and a query in there checks out a second one —
|
|
211
|
+
* concurrent posts would exhaust the pool. Reading outside means two
|
|
212
|
+
* refreshes can interleave, so the write goes through
|
|
213
|
+
* {@link supersedesFeedActivity} rather than landing whatever it read.
|
|
214
|
+
* Touches ONLY the counter columns: `revision`/`updated_at` are the
|
|
215
|
+
* stage-transition concurrency token.
|
|
216
|
+
*/
|
|
217
|
+
async refreshWorkItemFeedActivity({ orgId, factoryProjectId, workItemId, now = /* @__PURE__ */ new Date() }) {
|
|
218
|
+
const commentCount = await this.countForWorkItem({
|
|
219
|
+
orgId,
|
|
220
|
+
factoryProjectId,
|
|
221
|
+
workItemId
|
|
222
|
+
});
|
|
223
|
+
const [latest] = await this.ops.findMany("work_item_comments", {
|
|
224
|
+
org_id: orgId,
|
|
225
|
+
factory_project_id: factoryProjectId,
|
|
226
|
+
work_item_id: workItemId
|
|
227
|
+
}, {
|
|
228
|
+
orderBy: [["updated_at", "desc"]],
|
|
229
|
+
limit: 1
|
|
230
|
+
});
|
|
231
|
+
const feedActivityAt = latest?.updated_at ?? now;
|
|
232
|
+
await this.ops.updateAtomic("work_items", {
|
|
233
|
+
id: workItemId,
|
|
234
|
+
org_id: orgId,
|
|
235
|
+
factory_project_id: factoryProjectId
|
|
236
|
+
}, (row) => {
|
|
237
|
+
const next = {
|
|
238
|
+
commentCount,
|
|
239
|
+
feedActivityAt
|
|
240
|
+
};
|
|
241
|
+
const stored = row.feed_activity_at;
|
|
242
|
+
if (stored && !supersedesFeedActivity(next, {
|
|
243
|
+
commentCount: row.comment_count,
|
|
244
|
+
feedActivityAt: stored
|
|
245
|
+
})) return null;
|
|
246
|
+
return {
|
|
247
|
+
comment_count: commentCount,
|
|
248
|
+
feed_activity_at: feedActivityAt
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async get({ orgId, commentId }) {
|
|
253
|
+
const row = await this.ops.findOne("work_item_comments", {
|
|
254
|
+
id: commentId,
|
|
255
|
+
org_id: orgId
|
|
256
|
+
});
|
|
257
|
+
return row ? toComment(row) : null;
|
|
258
|
+
}
|
|
259
|
+
async listByIds({ orgId, ids }) {
|
|
260
|
+
if (ids.length === 0) return [];
|
|
261
|
+
return (await this.ops.findMany("work_item_comments", {
|
|
262
|
+
org_id: orgId,
|
|
263
|
+
id: { in: ids }
|
|
264
|
+
})).map(toComment);
|
|
265
|
+
}
|
|
266
|
+
async list(input) {
|
|
267
|
+
const anchored = input.around ? await this.#listAround(input, input.around) : void 0;
|
|
268
|
+
if (anchored) return anchored;
|
|
269
|
+
const limit = clampCommentLimit(input.limit);
|
|
270
|
+
const cursor = input.before ? decodeCommentCursor(input.before) : void 0;
|
|
271
|
+
const rows = await this.ops.findMany("work_item_comments", {
|
|
272
|
+
org_id: input.orgId,
|
|
273
|
+
factory_project_id: input.factoryProjectId,
|
|
274
|
+
work_item_id: input.workItemId
|
|
275
|
+
}, {
|
|
276
|
+
orderBy: [["occurred_at", "desc"], ["id", "desc"]],
|
|
277
|
+
limit: limit + 1,
|
|
278
|
+
...cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}
|
|
279
|
+
});
|
|
280
|
+
const comments = rows.slice(0, limit).map(toComment);
|
|
281
|
+
const hasMore = rows.length > limit;
|
|
282
|
+
const last = comments[comments.length - 1];
|
|
283
|
+
return {
|
|
284
|
+
comments,
|
|
285
|
+
...hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async #listAround({ orgId, factoryProjectId, workItemId }, commentId) {
|
|
289
|
+
const target = await this.get({
|
|
290
|
+
orgId,
|
|
291
|
+
commentId
|
|
292
|
+
});
|
|
293
|
+
if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return void 0;
|
|
294
|
+
const newer = await this.ops.findMany("work_item_comments", {
|
|
295
|
+
org_id: orgId,
|
|
296
|
+
factory_project_id: factoryProjectId,
|
|
297
|
+
work_item_id: workItemId
|
|
298
|
+
}, {
|
|
299
|
+
orderBy: [["occurred_at", "asc"], ["id", "asc"]],
|
|
300
|
+
limit: 101,
|
|
301
|
+
cursor: { values: [target.occurredAt, target.id] }
|
|
302
|
+
});
|
|
303
|
+
if (newer.length > MAX_PAGE_SIZE) return void 0;
|
|
304
|
+
const before = encodeCommentCursor(target);
|
|
305
|
+
const older = await this.list({
|
|
306
|
+
orgId,
|
|
307
|
+
factoryProjectId,
|
|
308
|
+
workItemId,
|
|
309
|
+
before,
|
|
310
|
+
limit: 1
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
comments: [...newer.map(toComment).reverse(), target],
|
|
314
|
+
...older.comments.length > 0 ? { nextCursor: before } : {}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Newest non-deleted comments for run-context injection, newest-first (the
|
|
319
|
+
* caller reverses for display order).
|
|
320
|
+
*/
|
|
321
|
+
async listRecent({ orgId, factoryProjectId, workItemId, limit }) {
|
|
322
|
+
return (await this.ops.findMany("work_item_comments", {
|
|
323
|
+
org_id: orgId,
|
|
324
|
+
factory_project_id: factoryProjectId,
|
|
325
|
+
work_item_id: workItemId,
|
|
326
|
+
deleted_at: null
|
|
327
|
+
}, {
|
|
328
|
+
orderBy: [["occurred_at", "desc"], ["id", "desc"]],
|
|
329
|
+
limit
|
|
330
|
+
})).map(toComment);
|
|
331
|
+
}
|
|
332
|
+
async edit(input) {
|
|
333
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
334
|
+
let rejection;
|
|
335
|
+
const updated = await this.ops.updateAtomic("work_item_comments", {
|
|
336
|
+
id: input.commentId,
|
|
337
|
+
org_id: input.orgId
|
|
338
|
+
}, (current) => {
|
|
339
|
+
if (current.deleted_at) {
|
|
340
|
+
rejection = "deleted";
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (input.expectedRevision !== void 0 && Number(current.revision) !== input.expectedRevision) {
|
|
344
|
+
rejection = "conflict";
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
body: input.body,
|
|
349
|
+
mentions: dedupeMentions(input.mentions ?? current.mentions),
|
|
350
|
+
edited_at: now,
|
|
351
|
+
revision: Number(current.revision) + 1,
|
|
352
|
+
updated_at: now
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
if (rejection === "conflict") return "conflict";
|
|
356
|
+
if (!updated || rejection) return null;
|
|
357
|
+
const comment = toComment(updated);
|
|
358
|
+
const existing = await this.#listMentionRows(comment.id);
|
|
359
|
+
const existingKeys = new Set(existing.map((mention) => mentionKey(toMentionRef(mention))));
|
|
360
|
+
const nextKeys = new Set(comment.mentions.map(mentionKey));
|
|
361
|
+
const skippedIds = /* @__PURE__ */ new Set([comment.author.id, ...input.editorId ? [input.editorId] : []]);
|
|
362
|
+
const addedMentions = comment.mentions.filter((mention) => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id));
|
|
363
|
+
const removedMentions = existing.map(toMentionRef).filter((mention) => !nextKeys.has(mentionKey(mention)));
|
|
364
|
+
await this.#writeMentionRows(comment, addedMentions, now);
|
|
365
|
+
for (const mention of removedMentions) await this.ops.deleteMany("work_item_comment_mentions", {
|
|
366
|
+
comment_id: comment.id,
|
|
367
|
+
mentioned_kind: mention.kind,
|
|
368
|
+
mentioned_id: mention.id
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
comment,
|
|
372
|
+
addedMentions,
|
|
373
|
+
removedMentions
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async softDelete({ orgId, commentId, deletedBy, now = /* @__PURE__ */ new Date() }) {
|
|
377
|
+
let alreadyDeleted = false;
|
|
378
|
+
const updated = await this.ops.updateAtomic("work_item_comments", {
|
|
379
|
+
id: commentId,
|
|
380
|
+
org_id: orgId
|
|
381
|
+
}, (current) => {
|
|
382
|
+
if (current.deleted_at) {
|
|
383
|
+
alreadyDeleted = true;
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
body: "",
|
|
388
|
+
mentions: [],
|
|
389
|
+
deleted_at: now,
|
|
390
|
+
deleted_by: deletedBy,
|
|
391
|
+
updated_at: now
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
if (!updated || alreadyDeleted) return null;
|
|
395
|
+
await this.ops.deleteMany("work_item_comment_mentions", { comment_id: commentId });
|
|
396
|
+
return toComment(updated);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Provenance write-back after an outbound publish. First platform wins, and
|
|
400
|
+
* an existing `source_key` is kept: replacing a `local:comment:<token>` one
|
|
401
|
+
* would let a client retry duplicate the row. So a web-born comment is never
|
|
402
|
+
* key-deduped against its own platform echo — the host's bot-sender check is
|
|
403
|
+
* the only echo layer for those rows (COR-1174).
|
|
404
|
+
*/
|
|
405
|
+
async attachExternalSource({ orgId, commentId, source }) {
|
|
406
|
+
const updated = await this.ops.updateAtomic("work_item_comments", {
|
|
407
|
+
id: commentId,
|
|
408
|
+
org_id: orgId
|
|
409
|
+
}, (current) => {
|
|
410
|
+
if (current.external_source) return null;
|
|
411
|
+
return {
|
|
412
|
+
external_source: source,
|
|
413
|
+
source_key: current.source_key ?? commentSourceKey({ externalSource: source }),
|
|
414
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
return updated ? toComment(updated) : null;
|
|
418
|
+
}
|
|
419
|
+
async listMentionsForComment(commentId) {
|
|
420
|
+
return this.#listMentionRows(commentId);
|
|
421
|
+
}
|
|
422
|
+
/** Keyset inbox read for the mention attention provider, newest-first. */
|
|
423
|
+
async listMentionsForUser({ orgId, factoryProjectId, userId, before, limit }) {
|
|
424
|
+
return (await this.ops.findMany("work_item_comment_mentions", {
|
|
425
|
+
org_id: orgId,
|
|
426
|
+
factory_project_id: factoryProjectId,
|
|
427
|
+
mentioned_id: userId
|
|
428
|
+
}, {
|
|
429
|
+
orderBy: [["occurred_at", "desc"], ["id", "desc"]],
|
|
430
|
+
limit,
|
|
431
|
+
...before ? { cursor: { values: [before.occurredAt, before.id] } } : {}
|
|
432
|
+
})).map(toMention);
|
|
433
|
+
}
|
|
434
|
+
async countForWorkItem({ orgId, factoryProjectId, workItemId }) {
|
|
435
|
+
if (!this.ops.count) throw new Error("[WorkItemCommentsStorage] storage backend does not support collection counts.");
|
|
436
|
+
return this.ops.count("work_item_comments", {
|
|
437
|
+
org_id: orgId,
|
|
438
|
+
factory_project_id: factoryProjectId,
|
|
439
|
+
work_item_id: workItemId,
|
|
440
|
+
deleted_at: null
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** Recent distinct comment authors of a project, for the roster fallback. */
|
|
444
|
+
async listRecentAuthors({ orgId, factoryProjectId, limit = 200 }) {
|
|
445
|
+
const rows = await this.ops.findMany("work_item_comments", {
|
|
446
|
+
org_id: orgId,
|
|
447
|
+
factory_project_id: factoryProjectId
|
|
448
|
+
}, {
|
|
449
|
+
orderBy: [["occurred_at", "desc"], ["id", "desc"]],
|
|
450
|
+
limit
|
|
451
|
+
});
|
|
452
|
+
const authors = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
if (authors.has(row.author_id)) continue;
|
|
455
|
+
authors.set(row.author_id, toComment(row).author);
|
|
456
|
+
}
|
|
457
|
+
return [...authors.values()];
|
|
458
|
+
}
|
|
459
|
+
async #listMentionRows(commentId) {
|
|
460
|
+
return (await this.ops.findMany("work_item_comment_mentions", { comment_id: commentId })).map(toMention);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Self-mentions never get rows: the join exists solely for the attention
|
|
464
|
+
* inbox, and `CollectionWhere` cannot express `author_id != userId` at read
|
|
465
|
+
* time, so the filter happens at write time.
|
|
466
|
+
*/
|
|
467
|
+
async #writeMentionRows(comment, mentions, occurredAt = comment.occurredAt) {
|
|
468
|
+
for (const mention of mentions) {
|
|
469
|
+
if (mention.id === comment.author.id) continue;
|
|
470
|
+
await this.ops.upsertOne("work_item_comment_mentions", [
|
|
471
|
+
"comment_id",
|
|
472
|
+
"mentioned_kind",
|
|
473
|
+
"mentioned_id"
|
|
474
|
+
], {
|
|
475
|
+
comment_id: comment.id,
|
|
476
|
+
mentioned_kind: mention.kind,
|
|
477
|
+
mentioned_id: mention.id,
|
|
478
|
+
author_id: comment.author.id,
|
|
479
|
+
org_id: comment.orgId,
|
|
480
|
+
factory_project_id: comment.factoryProjectId,
|
|
481
|
+
work_item_id: comment.workItemId,
|
|
482
|
+
occurred_at: occurredAt
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
//#endregion
|
|
488
|
+
export { CommentTokenConflictError, MAX_COMMENT_BODY_LENGTH, MAX_COMMENT_MENTIONS, MAX_COMMENT_QUOTE_LENGTH, WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WorkItemCommentsStorage, clampCommentLimit, commentBodyError, commentSourceKey, decodeCommentCursor, encodeCommentCursor, supersedesFeedActivity };
|
|
489
|
+
|
|
490
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":["#insertWithMentions","#writeMentionRows","#listAround","#listMentionRows"],"sources":["../../../../src/storage/domains/comments/base.ts"],"sourcesContent":["/**\n * Work-item comments domain — one message list per work item, materialized\n * from every source. Local creates and platform mirrors (Slack, Linear,\n * GitHub) share one idempotency story: `external_source` json + derived\n * `source_key` under a partial unique index, a local retry token being just\n * another source (`local:comment:<uuid>`).\n *\n * Ordering is caller-settable `occurred_at`, never insert time — ingest\n * backdates to the platform timestamp and retries arrive out of order.\n * Deletes are soft: the tombstone holds the ordering and its kept\n * `source_key` stops a redelivery from resurrecting the row.\n */\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\n\nimport type { ExternalWorkItemSource } from '../work-items/base.js';\nimport type { FactoryActorExternalIdentity, FactoryActorRef } from './actor.js';\nimport { WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport { WORK_ITEM_COMMENT_MENTIONS_SCHEMA, WORK_ITEM_COMMENTS_SCHEMA } from './schema.js';\n\nexport type WorkItemCommentKind = 'comment';\n\nexport interface FactoryMentionRef {\n kind: 'user';\n id: string;\n}\n\nexport interface WorkItemCommentReplyRef {\n commentId: string;\n quote?: string;\n authorId?: string;\n authorName?: string;\n}\n\nexport interface WorkItemCommentRow {\n id: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n kind: WorkItemCommentKind;\n body: string;\n bodyFormat: string;\n author: FactoryActorRef;\n replyTo: WorkItemCommentReplyRef | null;\n mentions: FactoryMentionRef[];\n externalSource: ExternalWorkItemSource | null;\n sourceKey: string | null;\n occurredAt: Date;\n editedAt: Date | null;\n deletedAt: Date | null;\n deletedBy: string | null;\n revision: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateWorkItemCommentInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n author: FactoryActorRef;\n body: string;\n bodyFormat?: string;\n replyTo?: WorkItemCommentReplyRef;\n mentions?: FactoryMentionRef[];\n externalSource?: ExternalWorkItemSource;\n /** Local idempotent-retry token; mutually exclusive with `externalSource`. */\n clientToken?: string;\n occurredAt?: Date;\n}\n\nexport interface EditWorkItemCommentInput {\n orgId: string;\n commentId: string;\n body: string;\n mentions?: FactoryMentionRef[];\n /** The acting user; their own handle never becomes a mention row. */\n editorId?: string;\n /** When set, the edit only lands if the row is still at this revision. */\n expectedRevision?: number;\n now?: Date;\n}\n\nexport interface EditWorkItemCommentResult {\n comment: WorkItemCommentRow;\n addedMentions: FactoryMentionRef[];\n removedMentions: FactoryMentionRef[];\n}\n\nexport interface ListWorkItemCommentsInput {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n before?: string;\n limit?: number;\n /**\n * Anchor the first page on this comment: it and every comment newer than it,\n * so a deep link opens on the target instead of paging back to find it.\n * Ignored when the comment is gone, or sits further back than one page holds.\n */\n around?: string;\n}\n\n/** The only two work-item columns the feed refresh reads or writes. */\ninterface WorkItemFeedColumns extends Record<string, unknown> {\n comment_count: number;\n feed_activity_at: Date | null;\n}\n\n/** A feed snapshot: how many comments the item had, and when it last moved. */\nexport interface FeedActivitySnapshot {\n commentCount: number;\n feedActivityAt: Date;\n}\n\n/**\n * The refresh reads its snapshot outside the write transaction, so two of them\n * can interleave and the loser can write last. This is the one rule that keeps\n * that from undoing a newer refresh: feed activity never moves backwards, and\n * on the same stamp the fuller count wins.\n *\n * Known ceiling: a soft delete sharing a millisecond with a create leaves the\n * count one high until the next feed mutation recounts. Closing that needs the\n * aggregate read inside the write transaction, which the ops layer cannot do\n * without checking out a second pool connection per open transaction.\n */\nexport function supersedesFeedActivity(next: FeedActivitySnapshot, stored: FeedActivitySnapshot): boolean {\n const drift = next.feedActivityAt.getTime() - stored.feedActivityAt.getTime();\n return drift > 0 || (drift === 0 && next.commentCount >= stored.commentCount);\n}\n\nexport interface WorkItemCommentPage {\n comments: WorkItemCommentRow[];\n nextCursor?: string;\n}\n\nexport interface WorkItemMentionRow {\n id: string;\n commentId: string;\n mentionedKind: 'user';\n mentionedId: string;\n authorId: string;\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n occurredAt: Date;\n}\n\nexport const MAX_COMMENT_BODY_LENGTH = 16_000;\nexport const MAX_COMMENT_QUOTE_LENGTH = 500;\nexport const MAX_COMMENT_MENTIONS = 20;\n\n/** The one body policy: HTTP parsing and the service both reject on it. */\nexport function commentBodyError(body: string): string | undefined {\n if (!body.trim()) return 'Comment body must not be empty.';\n if (body.length > MAX_COMMENT_BODY_LENGTH)\n return `Comment body must be at most ${MAX_COMMENT_BODY_LENGTH} characters.`;\n return undefined;\n}\n\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 100;\n\nexport function clampCommentLimit(limit: number | undefined): number {\n const normalized = typeof limit === 'number' && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;\n return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);\n}\n\nexport function encodeCommentCursor(row: WorkItemCommentRow): string {\n return `${row.occurredAt.toISOString()}_${row.id}`;\n}\n\nexport function decodeCommentCursor(cursor: string): { occurredAt: Date; id: string } | undefined {\n const sep = cursor.lastIndexOf('_');\n if (sep <= 0) return undefined;\n const occurredAt = new Date(cursor.slice(0, sep));\n const id = cursor.slice(sep + 1);\n if (Number.isNaN(occurredAt.getTime()) || !id) return undefined;\n return { occurredAt, id };\n}\n\nexport function commentSourceKey(input: {\n externalSource?: ExternalWorkItemSource;\n clientToken?: string;\n}): string | null {\n if (input.externalSource) {\n const source = input.externalSource;\n return `${source.integrationId}:${source.type}:${source.externalId}`;\n }\n if (input.clientToken) return `local:comment:${input.clientToken}`;\n return null;\n}\n\n/** A `clientToken` replay that resolved to a different work item or author. */\nexport class CommentTokenConflictError extends Error {\n constructor() {\n super('Client token already used by a different comment.');\n this.name = 'CommentTokenConflictError';\n }\n}\n\ninterface WorkItemCommentDbRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n kind: WorkItemCommentKind;\n body: string;\n body_format: string;\n author_kind: FactoryActorRef['kind'];\n author_id: string;\n author_display_name: string | null;\n author_avatar_url: string | null;\n author_external: FactoryActorExternalIdentity | null;\n reply_to_comment_id: string | null;\n reply_quote: string | null;\n reply_to_author_id: string | null;\n reply_to_author_name: string | null;\n mentions: FactoryMentionRef[];\n external_source: ExternalWorkItemSource | null;\n source_key: string | null;\n occurred_at: Date;\n edited_at: Date | null;\n deleted_at: Date | null;\n deleted_by: string | null;\n revision: number;\n created_at: Date;\n updated_at: Date;\n}\n\ninterface WorkItemMentionDbRow extends Record<string, unknown> {\n id: string;\n comment_id: string;\n mentioned_kind: 'user';\n mentioned_id: string;\n author_id: string;\n org_id: string;\n factory_project_id: string;\n work_item_id: string;\n occurred_at: Date;\n}\n\nfunction toComment(row: WorkItemCommentDbRow): WorkItemCommentRow {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n kind: row.kind,\n body: row.body,\n bodyFormat: row.body_format,\n author: {\n kind: row.author_kind,\n id: row.author_id,\n ...(row.author_display_name ? { displayName: row.author_display_name } : {}),\n ...(row.author_avatar_url ? { avatarUrl: row.author_avatar_url } : {}),\n ...(row.author_external ? { external: row.author_external } : {}),\n },\n replyTo: row.reply_to_comment_id\n ? {\n commentId: row.reply_to_comment_id,\n ...(row.reply_quote ? { quote: row.reply_quote } : {}),\n ...(row.reply_to_author_id ? { authorId: row.reply_to_author_id } : {}),\n ...(row.reply_to_author_name ? { authorName: row.reply_to_author_name } : {}),\n }\n : null,\n mentions: row.mentions,\n externalSource: row.external_source,\n sourceKey: row.source_key,\n occurredAt: row.occurred_at,\n editedAt: row.edited_at,\n deletedAt: row.deleted_at,\n deletedBy: row.deleted_by,\n revision: row.revision,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toMention(row: WorkItemMentionDbRow): WorkItemMentionRow {\n return {\n id: row.id,\n commentId: row.comment_id,\n mentionedKind: row.mentioned_kind,\n mentionedId: row.mentioned_id,\n authorId: row.author_id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n workItemId: row.work_item_id,\n occurredAt: row.occurred_at,\n };\n}\n\nfunction toMentionRef(row: WorkItemMentionRow): FactoryMentionRef {\n return { kind: row.mentionedKind, id: row.mentionedId };\n}\n\nfunction mentionKey(mention: { kind: string; id: string }): string {\n return `${mention.kind}\\0${mention.id}`;\n}\n\nfunction dedupeMentions(mentions: FactoryMentionRef[] | undefined): FactoryMentionRef[] {\n if (!mentions?.length) return [];\n return [...new Map(mentions.map(mention => [mentionKey(mention), mention])).values()];\n}\n\nexport class WorkItemCommentsStorage extends FactoryStorageDomain {\n constructor() {\n super('work-item-comments');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([WORK_ITEM_COMMENTS_SCHEMA, WORK_ITEM_COMMENT_MENTIONS_SCHEMA]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('work_item_comment_mentions', {});\n await this.ops.deleteMany('work_item_comments', {});\n }\n\n async create(input: CreateWorkItemCommentInput): Promise<WorkItemCommentRow> {\n const now = new Date();\n const occurredAt = input.occurredAt ?? now;\n const sourceKey = commentSourceKey(input);\n const author = input.author;\n const row: Partial<WorkItemCommentDbRow> = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n kind: 'comment',\n body: input.body,\n body_format: input.bodyFormat ?? 'markdown',\n author_kind: author.kind,\n author_id: author.id,\n author_display_name: author.displayName ?? null,\n author_avatar_url: author.avatarUrl ?? null,\n author_external: author.external ?? null,\n reply_to_comment_id: input.replyTo?.commentId ?? null,\n reply_quote: input.replyTo?.quote ?? null,\n reply_to_author_id: input.replyTo?.authorId ?? null,\n reply_to_author_name: input.replyTo?.authorName ?? null,\n mentions: dedupeMentions(input.mentions),\n external_source: input.externalSource ?? null,\n source_key: sourceKey,\n occurred_at: occurredAt,\n edited_at: null,\n deleted_at: null,\n deleted_by: null,\n revision: 1,\n created_at: now,\n updated_at: now,\n };\n\n // Insert-or-recover, never upsert: a replayed create must return the\n // existing row untouched (an upsert would clobber a later edit's body).\n // A local token resolving to another item or author is a conflict, not a\n // recovery. External keys stay lenient: a redelivery after a thread\n // re-link maps to a new item id and must still no-op.\n if (sourceKey) {\n const sourceWhere = {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n source_key: sourceKey,\n };\n const recover = (found: WorkItemCommentDbRow): WorkItemCommentRow => {\n if (!input.externalSource && (found.work_item_id !== input.workItemId || found.author_id !== author.id)) {\n throw new CommentTokenConflictError();\n }\n return toComment(found);\n };\n const existing = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (existing) return recover(existing);\n try {\n return await this.#insertWithMentions(row);\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n const raced = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', sourceWhere);\n if (!raced) throw error;\n return recover(raced);\n }\n }\n\n return this.#insertWithMentions(row);\n }\n\n async #insertWithMentions(row: Partial<WorkItemCommentDbRow>): Promise<WorkItemCommentRow> {\n const comment = toComment(await this.ops.insertOne<WorkItemCommentDbRow>('work_item_comments', row));\n await this.#writeMentionRows(comment, comment.mentions);\n return comment;\n }\n\n /**\n * Idempotent counter refresh on the parent work item: both columns are read\n * back off the comments (never incremented or stamped with the wall clock —\n * replays and races double an increment, and a replayed create would move a\n * work item in the feed without adding a comment; a read-back can't drift).\n * Read BEFORE `updateAtomic`: its mutator runs inside an open transaction\n * holding a pool connection, and a query in there checks out a second one —\n * concurrent posts would exhaust the pool. Reading outside means two\n * refreshes can interleave, so the write goes through\n * {@link supersedesFeedActivity} rather than landing whatever it read.\n * Touches ONLY the counter columns: `revision`/`updated_at` are the\n * stage-transition concurrency token.\n */\n async refreshWorkItemFeedActivity({\n orgId,\n factoryProjectId,\n workItemId,\n now = new Date(),\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n now?: Date;\n }): Promise<void> {\n const commentCount = await this.countForWorkItem({ orgId, factoryProjectId, workItemId });\n // Every feed mutation stamps `updated_at`, edits and tombstones included,\n // so the newest one is when this feed last moved.\n const [latest] = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n { orderBy: [['updated_at', 'desc']], limit: 1 },\n );\n const feedActivityAt = latest?.updated_at ?? now;\n await this.ops.updateAtomic<WorkItemFeedColumns>(\n 'work_items',\n { id: workItemId, org_id: orgId, factory_project_id: factoryProjectId },\n row => {\n const next = { commentCount, feedActivityAt };\n const stored = row.feed_activity_at;\n if (stored && !supersedesFeedActivity(next, { commentCount: row.comment_count, feedActivityAt: stored })) {\n return null;\n }\n return { comment_count: commentCount, feed_activity_at: feedActivityAt };\n },\n );\n }\n\n async get({ orgId, commentId }: { orgId: string; commentId: string }): Promise<WorkItemCommentRow | null> {\n const row = await this.ops.findOne<WorkItemCommentDbRow>('work_item_comments', {\n id: commentId,\n org_id: orgId,\n });\n return row ? toComment(row) : null;\n }\n\n async listByIds({ orgId, ids }: { orgId: string; ids: string[] }): Promise<WorkItemCommentRow[]> {\n if (ids.length === 0) return [];\n const rows = await this.ops.findMany<WorkItemCommentDbRow>('work_item_comments', {\n org_id: orgId,\n id: { in: ids },\n });\n return rows.map(toComment);\n }\n\n async list(input: ListWorkItemCommentsInput): Promise<WorkItemCommentPage> {\n const anchored = input.around ? await this.#listAround(input, input.around) : undefined;\n if (anchored) return anchored;\n const limit = clampCommentLimit(input.limit);\n const cursor = input.before ? decodeCommentCursor(input.before) : undefined;\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: input.orgId,\n factory_project_id: input.factoryProjectId,\n work_item_id: input.workItemId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit: limit + 1,\n ...(cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}),\n },\n );\n const comments = rows.slice(0, limit).map(toComment);\n const hasMore = rows.length > limit;\n const last = comments[comments.length - 1];\n return {\n comments,\n ...(hasMore && last ? { nextCursor: encodeCommentCursor(last) } : {}),\n };\n }\n\n async #listAround(\n { orgId, factoryProjectId, workItemId }: ListWorkItemCommentsInput,\n commentId: string,\n ): Promise<WorkItemCommentPage | undefined> {\n const target = await this.get({ orgId, commentId });\n if (!target || target.factoryProjectId !== factoryProjectId || target.workItemId !== workItemId) return undefined;\n\n const newer = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId, work_item_id: workItemId },\n {\n orderBy: [\n ['occurred_at', 'asc'],\n ['id', 'asc'],\n ],\n limit: MAX_PAGE_SIZE + 1,\n cursor: { values: [target.occurredAt, target.id] },\n },\n );\n if (newer.length > MAX_PAGE_SIZE) return undefined;\n\n const before = encodeCommentCursor(target);\n const older = await this.list({ orgId, factoryProjectId, workItemId, before, limit: 1 });\n return {\n comments: [...newer.map(toComment).reverse(), target],\n ...(older.comments.length > 0 ? { nextCursor: before } : {}),\n };\n }\n\n /**\n * Newest non-deleted comments for run-context injection, newest-first (the\n * caller reverses for display order).\n */\n async listRecent({\n orgId,\n factoryProjectId,\n workItemId,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n limit: number;\n }): Promise<WorkItemCommentRow[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n return rows.map(toComment);\n }\n\n async edit(input: EditWorkItemCommentInput): Promise<EditWorkItemCommentResult | 'conflict' | null> {\n const now = input.now ?? new Date();\n let rejection: 'deleted' | 'conflict' | undefined;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: input.commentId, org_id: input.orgId },\n current => {\n if (current.deleted_at) {\n rejection = 'deleted';\n return null;\n }\n if (input.expectedRevision !== undefined && Number(current.revision) !== input.expectedRevision) {\n rejection = 'conflict';\n return null;\n }\n return {\n body: input.body,\n mentions: dedupeMentions(input.mentions ?? current.mentions),\n edited_at: now,\n revision: Number(current.revision) + 1,\n updated_at: now,\n };\n },\n );\n if (rejection === 'conflict') return 'conflict';\n if (!updated || rejection) return null;\n const comment = toComment(updated);\n\n const existing = await this.#listMentionRows(comment.id);\n const existingKeys = new Set(existing.map(mention => mentionKey(toMentionRef(mention))));\n const nextKeys = new Set(comment.mentions.map(mentionKey));\n // Self-mentions (author, or the acting editor) never become rows, so they\n // must not report as \"added\" either — they would re-report on every edit.\n const skippedIds = new Set([comment.author.id, ...(input.editorId ? [input.editorId] : [])]);\n const addedMentions = comment.mentions.filter(\n mention => !existingKeys.has(mentionKey(mention)) && !skippedIds.has(mention.id),\n );\n const removedMentions = existing.map(toMentionRef).filter(mention => !nextKeys.has(mentionKey(mention)));\n\n // Stamped with the edit time, not the comment's creation time: the inbox\n // is a keyset on `occurred_at`, and a backdated row lands buried under\n // everything the user already saw.\n await this.#writeMentionRows(comment, addedMentions, now);\n for (const mention of removedMentions) {\n await this.ops.deleteMany('work_item_comment_mentions', {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n });\n }\n return { comment, addedMentions, removedMentions };\n }\n\n async softDelete({\n orgId,\n commentId,\n deletedBy,\n now = new Date(),\n }: {\n orgId: string;\n commentId: string;\n deletedBy: string;\n now?: Date;\n }): Promise<WorkItemCommentRow | null> {\n let alreadyDeleted = false;\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.deleted_at) {\n alreadyDeleted = true;\n return null;\n }\n return {\n body: '',\n mentions: [],\n deleted_at: now,\n deleted_by: deletedBy,\n updated_at: now,\n };\n },\n );\n if (!updated || alreadyDeleted) return null;\n await this.ops.deleteMany('work_item_comment_mentions', { comment_id: commentId });\n return toComment(updated);\n }\n\n /**\n * Provenance write-back after an outbound publish. First platform wins, and\n * an existing `source_key` is kept: replacing a `local:comment:<token>` one\n * would let a client retry duplicate the row. So a web-born comment is never\n * key-deduped against its own platform echo — the host's bot-sender check is\n * the only echo layer for those rows (COR-1174).\n */\n async attachExternalSource({\n orgId,\n commentId,\n source,\n }: {\n orgId: string;\n commentId: string;\n source: ExternalWorkItemSource;\n }): Promise<WorkItemCommentRow | null> {\n const updated = await this.ops.updateAtomic<WorkItemCommentDbRow>(\n 'work_item_comments',\n { id: commentId, org_id: orgId },\n current => {\n if (current.external_source) return null;\n return {\n external_source: source,\n source_key: current.source_key ?? commentSourceKey({ externalSource: source }),\n updated_at: new Date(),\n };\n },\n );\n return updated ? toComment(updated) : null;\n }\n\n async listMentionsForComment(commentId: string): Promise<WorkItemMentionRow[]> {\n return this.#listMentionRows(commentId);\n }\n\n /** Keyset inbox read for the mention attention provider, newest-first. */\n async listMentionsForUser({\n orgId,\n factoryProjectId,\n userId,\n before,\n limit,\n }: {\n orgId: string;\n factoryProjectId: string;\n userId: string;\n before?: { occurredAt: Date; id: string };\n limit: number;\n }): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n mentioned_id: userId,\n },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n ...(before ? { cursor: { values: [before.occurredAt, before.id] } } : {}),\n },\n );\n return rows.map(toMention);\n }\n\n async countForWorkItem({\n orgId,\n factoryProjectId,\n workItemId,\n }: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n }): Promise<number> {\n if (!this.ops.count) {\n throw new Error('[WorkItemCommentsStorage] storage backend does not support collection counts.');\n }\n return this.ops.count('work_item_comments', {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n work_item_id: workItemId,\n deleted_at: null,\n });\n }\n\n /** Recent distinct comment authors of a project, for the roster fallback. */\n async listRecentAuthors({\n orgId,\n factoryProjectId,\n limit = 200,\n }: {\n orgId: string;\n factoryProjectId: string;\n limit?: number;\n }): Promise<FactoryActorRef[]> {\n const rows = await this.ops.findMany<WorkItemCommentDbRow>(\n 'work_item_comments',\n { org_id: orgId, factory_project_id: factoryProjectId },\n {\n orderBy: [\n ['occurred_at', 'desc'],\n ['id', 'desc'],\n ],\n limit,\n },\n );\n const authors = new Map<string, FactoryActorRef>();\n for (const row of rows) {\n if (authors.has(row.author_id)) continue;\n authors.set(row.author_id, toComment(row).author);\n }\n return [...authors.values()];\n }\n\n async #listMentionRows(commentId: string): Promise<WorkItemMentionRow[]> {\n const rows = await this.ops.findMany<WorkItemMentionDbRow>('work_item_comment_mentions', {\n comment_id: commentId,\n });\n return rows.map(toMention);\n }\n\n /**\n * Self-mentions never get rows: the join exists solely for the attention\n * inbox, and `CollectionWhere` cannot express `author_id != userId` at read\n * time, so the filter happens at write time.\n */\n async #writeMentionRows(\n comment: WorkItemCommentRow,\n mentions: FactoryMentionRef[],\n occurredAt = comment.occurredAt,\n ): Promise<void> {\n for (const mention of mentions) {\n if (mention.id === comment.author.id) continue;\n await this.ops.upsertOne<WorkItemMentionDbRow>(\n 'work_item_comment_mentions',\n ['comment_id', 'mentioned_kind', 'mentioned_id'],\n {\n comment_id: comment.id,\n mentioned_kind: mention.kind,\n mentioned_id: mention.id,\n author_id: comment.author.id,\n org_id: comment.orgId,\n factory_project_id: comment.factoryProjectId,\n work_item_id: comment.workItemId,\n occurred_at: occurredAt,\n },\n );\n }\n }\n}\n"],"mappings":"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"}
|