diff --git a/apps/server/src/common/decorators/auth-provenance.decorator.ts b/apps/server/src/common/decorators/auth-provenance.decorator.ts index 3e49b6d4..8baa592b 100644 --- a/apps/server/src/common/decorators/auth-provenance.decorator.ts +++ b/apps/server/src/common/decorators/auth-provenance.decorator.ts @@ -13,6 +13,34 @@ export interface AuthProvenanceData { aiChatId: string | null; } +/** + * Agent-edit write-stamp fields for a repository insert/update (#143 review). + * Spread into the row being written: for an agent it stamps the `*Source` + * column 'agent' and the AI-chat id; for a normal user it returns `{}` so the + * column keeps its default ('user'). The only per-table variation is the column + * names, passed as `sourceKey`/`chatKey`, so the agent-stamp idiom lives in ONE + * place instead of being hand-reimplemented at every write site (where a wrong + * literal or a forgotten `aiChatId` could drift). + * + * insertComment({ ..., ...agentSourceFields(p, 'createdSource', 'aiChatId') }) + * updatePage({ ..., ...agentSourceFields(p, 'lastUpdatedSource', 'lastUpdatedAiChatId') }) + * + * Does NOT cover sites that must CLEAR the source on a non-agent action (e.g. + * comment un-resolve, which writes an explicit null) — those keep their own + * conditional; nor the collab persistence path (its own sticky-window logic). + */ +export function agentSourceFields( + provenance: AuthProvenanceData | undefined, + sourceKey: S, + chatKey: C, +): Partial & Record> { + if (provenance?.actor !== 'agent') return {}; + return { + [sourceKey]: 'agent', + [chatKey]: provenance.aiChatId, + } as Partial & Record>; +} + /** * Resolve the request's provenance. Defaults to a 'user' actor when the claim * is absent (e.g. an endpoint reached without going through the access-token diff --git a/apps/server/src/core/comment/comment.service.ts b/apps/server/src/core/comment/comment.service.ts index d88c5ffd..579438ef 100644 --- a/apps/server/src/core/comment/comment.service.ts +++ b/apps/server/src/core/comment/comment.service.ts @@ -22,7 +22,10 @@ import { ICommentResolvedNotificationJob, } from '../../integrations/queue/constants/queue.interface'; import { WsService } from '../../ws/ws.service'; -import { AuthProvenanceData } from '../../common/decorators/auth-provenance.decorator'; +import { + AuthProvenanceData, + agentSourceFields, +} from '../../common/decorators/auth-provenance.decorator'; @Injectable() export class CommentService { @@ -60,7 +63,6 @@ export class CommentService { ) { const { page, workspaceId, user } = opts; const commentContent = JSON.parse(createCommentDto.content); - const isAgent = provenance?.actor === 'agent'; if (createCommentDto.parentCommentId) { const parentComment = await this.commentRepo.findById( @@ -87,9 +89,7 @@ export class CommentService { spaceId: page.spaceId, // Agent-edit provenance: the user stays creatorId; this only annotates the // source. Normal user requests leave the column default ('user'). - ...(isAgent - ? { createdSource: 'agent', aiChatId: provenance.aiChatId } - : {}), + ...agentSourceFields(provenance, 'createdSource', 'aiChatId'), }); if (createCommentDto.yjsSelection) { diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 6ea00188..88ef01c8 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -57,7 +57,10 @@ import { WatcherService } from '../../watcher/watcher.service'; import { sql } from 'kysely'; import { TransclusionService } from '../transclusion/transclusion.service'; import { remapPageEmbedSourceId } from '../transclusion/utils/transclusion-prosemirror.util'; -import { AuthProvenanceData } from '../../../common/decorators/auth-provenance.decorator'; +import { + AuthProvenanceData, + agentSourceFields, +} from '../../../common/decorators/auth-provenance.decorator'; @Injectable() export class PageService { @@ -135,7 +138,6 @@ export class PageService { ydoc = createYdocFromJson(prosemirrorJson); } - const isAgent = provenance?.actor === 'agent'; const page = await this.pageRepo.insertPage({ slugId: generateSlugId(), @@ -153,12 +155,7 @@ export class PageService { // Agent-edit provenance. The human stays the responsible author // (creatorId/lastUpdatedById); these only annotate the source. A normal // user request leaves the column default ('user'). - ...(isAgent - ? { - lastUpdatedSource: 'agent', - lastUpdatedAiChatId: provenance.aiChatId, - } - : {}), + ...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'), content, textContent, ydoc, @@ -231,7 +228,6 @@ export class PageService { contributors.add(user.id); const contributorIds = Array.from(contributors); - const isAgent = provenance?.actor === 'agent'; // Detect a real title/icon change so the WS tree listener can broadcast an // `updateOne` to the space (rename / icon swap) WITHOUT re-broadcasting on a @@ -251,12 +247,7 @@ export class PageService { lastUpdatedById: user.id, // Agent-edit provenance: annotate the source without changing the // responsible author. A normal user request leaves the column default. - ...(isAgent - ? { - lastUpdatedSource: 'agent', - lastUpdatedAiChatId: provenance.aiChatId, - } - : {}), + ...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'), updatedAt: new Date(), contributorIds: contributorIds, }, @@ -443,7 +434,6 @@ export class PageService { provenance?: AuthProvenanceData, ) { let childPageIds: string[] = []; - const isAgent = provenance?.actor === 'agent'; const allPages = await this.pageRepo.getPageAndDescendants(rootPage.id, { includeContent: false, @@ -490,12 +480,7 @@ export class PageService { // Agent-edit provenance on the moved root page. Child pages are bulk // re-parented to the new space (no content change), so the marker is // stamped on the root the agent acted on. Normal user: no change. - ...(isAgent - ? { - lastUpdatedSource: 'agent', - lastUpdatedAiChatId: provenance.aiChatId, - } - : {}), + ...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'), }, rootPage.id, trx, @@ -949,7 +934,6 @@ export class PageService { } } - const isAgent = provenance?.actor === 'agent'; const updateResult = await this.pageRepo.updatePage( { @@ -957,12 +941,7 @@ export class PageService { parentPageId: parentPageId, // Agent-edit provenance: annotate the source on an agent move. A normal // user request leaves the column default ('user'). - ...(isAgent - ? { - lastUpdatedSource: 'agent', - lastUpdatedAiChatId: provenance.aiChatId, - } - : {}), + ...agentSourceFields(provenance, 'lastUpdatedSource', 'lastUpdatedAiChatId'), }, dto.pageId, );