feat: add /api/messages/images endpoint + ImageGrid fetch
Backend: - New GET /api/messages/images?guildId=&limit= endpoint - Queries attachments table for image/* MIME type, returns distinct messages - Repository uses Drizzle subquery + join pattern Frontend: - New get_images() API client function - ImageGrid fetches from /api/messages/images when Images tab selected - Separate image_messages signal, fetched on tab switch Deploy: hot-patched JS/WASM files to running containers
This commit is contained in:
@@ -50,6 +50,24 @@ export function handleGetMessageById(
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
export function handleGetImageMessages(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) {
|
||||
return asyncHandler(async (req: Request, res: Response) => {
|
||||
const guildId = requireParam(
|
||||
req.query.guildId as string,
|
||||
"query parameter",
|
||||
"guildId",
|
||||
);
|
||||
const limit = Number(req.query.limit) || 50;
|
||||
logger.debug({ guildId, limit }, "Handling get image messages");
|
||||
const result = await messagesService.getImageMessages(guildId, limit);
|
||||
res.json(result);
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
export function handleGetAttachmentsByChannel(
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageResult } from "@bete/shared";
|
||||
import { pgAttachmentsTable, pgMessagesTable } from "@bete/shared";
|
||||
import { createChildLogger } from "@bete/shared/logger";
|
||||
import { and, desc, eq, inArray, lt, ne, type SQL } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, like, lt, ne, type SQL } from "drizzle-orm";
|
||||
import { getDatabase } from "../../shared/database/index.js";
|
||||
import { mapMessageRow } from "../../shared/utils/messageMapper.js";
|
||||
import type {
|
||||
@@ -290,6 +290,43 @@ export class MessagesRepository {
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
}
|
||||
|
||||
async getImageMessages(
|
||||
guildId: string,
|
||||
limit: number = 50,
|
||||
): Promise<PageResult<ReturnType<typeof mapMessageRow>>> {
|
||||
const db = getDatabase();
|
||||
|
||||
// Subquery: find distinct message_ids from attachments with image MIME type
|
||||
const imageMsgIds = db
|
||||
.select({ id: pgAttachmentsTable.message_id })
|
||||
.from(pgAttachmentsTable)
|
||||
.where(
|
||||
and(
|
||||
eq(pgAttachmentsTable.guild_id, guildId),
|
||||
like(pgAttachmentsTable.type, "image/%"),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(pgAttachmentsTable.created_at))
|
||||
.limit(limit + 1);
|
||||
|
||||
// Fetch full message rows for those IDs
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(pgMessagesTable)
|
||||
.where(inArray(pgMessagesTable.id, imageMsgIds))
|
||||
.orderBy(desc(pgMessagesTable.created_at))
|
||||
.limit(limit + 1);
|
||||
|
||||
const data = rows
|
||||
.slice(0, limit)
|
||||
.map((r) => mapMessageRow(r as Record<string, unknown>));
|
||||
const nextCursor =
|
||||
rows.length > limit ? String(rows[limit].created_at) : null;
|
||||
|
||||
logger.debug({ count: data.length, nextCursor }, "Found image messages");
|
||||
return { data, nextCursor };
|
||||
}
|
||||
|
||||
async getAttachmentsByChannel(
|
||||
channelId: string,
|
||||
query: MessageQuery,
|
||||
|
||||
@@ -4,6 +4,7 @@ import express from "express";
|
||||
import { asyncHandler } from "../../shared/middlewares/index.js";
|
||||
import {
|
||||
handleGetAttachmentsByChannel,
|
||||
handleGetImageMessages,
|
||||
handleGetMessageById,
|
||||
handleGetMessagesByChannel,
|
||||
handleListMessages,
|
||||
@@ -31,6 +32,11 @@ const reanalyzeBatchInFlight = new Set<string>();
|
||||
export function createMessagesRouter(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/messages/images - Get messages with image attachments
|
||||
// MUST be registered BEFORE /messages/:channelId so "images" is not
|
||||
// captured as a channelId param.
|
||||
router.get("/messages/images", handleGetImageMessages);
|
||||
|
||||
// GET /api/messages - List messages
|
||||
router.get("/messages", handleListMessages);
|
||||
|
||||
|
||||
@@ -46,6 +46,18 @@ export class MessagesService {
|
||||
return messagesRepository.getAttachmentsByChannel(channelId, query);
|
||||
}
|
||||
|
||||
async getImageMessages(
|
||||
guildId: string,
|
||||
limit?: number,
|
||||
): Promise<ReturnType<typeof messagesRepository.getImageMessages>> {
|
||||
if (!guildId) {
|
||||
throw new ValidationError("guildId is required");
|
||||
}
|
||||
|
||||
logger.debug({ guildId, limit }, "Getting image messages");
|
||||
return messagesRepository.getImageMessages(guildId, limit);
|
||||
}
|
||||
|
||||
async markForReanalysis(id: string): Promise<void> {
|
||||
if (!id) {
|
||||
throw new ValidationError("message ID is required");
|
||||
|
||||
Reference in New Issue
Block a user