* Cursor is `createdAt` (immutable) but rows themselves are mutable — * `messages`, `title`, `lastSeenAt`, etc. are updated in-place over the chat's * lifetime. This means a chat exported once will not be re-exported when its * messages change. Consumers who need the latest state should periodica
(input: SourcePageInput)
| 49 | * design and `data-drains` is not a CDC pipeline. |
| 50 | */ |
| 51 | async function* pages(input: SourcePageInput): AsyncIterable<CopilotChatRow[]> { |
| 52 | const workspaceIds = await getOrganizationWorkspaceIds(input.organizationId) |
| 53 | if (workspaceIds.length === 0) return |
| 54 | |
| 55 | let cursor = decodeTimeCursor(input.cursor) |
| 56 | while (!input.signal.aborted) { |
| 57 | const cursorClause = timeCursorPredicate(copilotChats.createdAt, copilotChats.id, cursor) |
| 58 | |
| 59 | const metaRows = await dbReplica |
| 60 | .select(chatColumns) |
| 61 | .from(copilotChats) |
| 62 | .where( |
| 63 | and( |
| 64 | inArray(copilotChats.workspaceId, workspaceIds), |
| 65 | timeCursorStabilityBound(copilotChats.createdAt), |
| 66 | cursorClause |
| 67 | ) |
| 68 | ) |
| 69 | .orderBy(...timeCursorOrderBy(copilotChats.createdAt, copilotChats.id)) |
| 70 | .limit(input.chunkSize) |
| 71 | |
| 72 | if (metaRows.length === 0) return |
| 73 | |
| 74 | const chatIds = metaRows.map((r) => r.id) |
| 75 | const messageRows = await dbReplica |
| 76 | .select({ chatId: copilotMessages.chatId, content: copilotMessages.content }) |
| 77 | .from(copilotMessages) |
| 78 | .where(and(inArray(copilotMessages.chatId, chatIds), isNull(copilotMessages.deletedAt))) |
| 79 | .orderBy( |
| 80 | asc(copilotMessages.chatId), |
| 81 | sql`${copilotMessages.seq} asc nulls last`, |
| 82 | asc(copilotMessages.createdAt), |
| 83 | asc(copilotMessages.id) |
| 84 | ) |
| 85 | const messagesByChat = new Map<string, unknown[]>() |
| 86 | for (const m of messageRows) { |
| 87 | const existing = messagesByChat.get(m.chatId) |
| 88 | if (existing) existing.push(m.content) |
| 89 | else messagesByChat.set(m.chatId, [m.content]) |
| 90 | } |
| 91 | |
| 92 | const rows: CopilotChatRow[] = metaRows.map((r) => ({ |
| 93 | ...r, |
| 94 | messages: messagesByChat.get(r.id) ?? [], |
| 95 | })) |
| 96 | |
| 97 | yield rows |
| 98 | const last = metaRows[metaRows.length - 1] |
| 99 | cursor = { ts: last.createdAt.toISOString(), id: last.id } |
| 100 | if (metaRows.length < input.chunkSize) return |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | export const copilotChatsSource: DrainSource<CopilotChatRow> = { |
| 105 | type: 'copilot_chats', |
nothing calls this directly
no test coverage detected