feat: wire embeds into all message query and creation paths

Integrate embed infrastructure into the complete message flow:
- messages.ts: batch-fetch embeds in GET, resolve on POST, re-resolve on PATCH
- dm.ts: same pattern for DM messages with isDm=true
- search.ts: include embeds in all 4 search/around endpoints
- events.ts: embed resolution in WS message create/edit for both space and DM
- Fix embedClassifier.ts type errors (regex match undefined → null)
- Add embeds: [] to all inline MessageWithUser/DmMessageWithUser constructions
- Add embeds: [] to chatStore optimistic message
This commit is contained in:
Jannis Braun
2026-03-20 23:41:53 +01:00
parent 5fe25fb077
commit 01ee7ab67a
6 changed files with 93 additions and 13 deletions
+26 -2
View File
@@ -16,9 +16,11 @@ import {
type PaginatedQuery,
type Attachment,
type Reaction,
type Embed,
} from '@backspace/shared';
import { sanitizeUser } from '../utils/sanitize.js';
import { deleteUploadFile, deleteAttachmentFiles } from '../utils/fileCleanup.js';
import { fetchDmEmbedsForMessages, resolveEmbeds, reResolveEmbeds, embedRowToEmbed } from '../utils/embedResolver.js';
/**
* Batch-fetch reactions for a set of DM message IDs.
@@ -67,6 +69,7 @@ export function buildDmMessageWithUser(
attachmentRows: (typeof schema.attachments.$inferSelect)[],
reactions: Reaction[] = [],
replyTo: DmMessageWithUser | null = null,
embedRows: (typeof schema.embeds.$inferSelect)[] = [],
): DmMessageWithUser {
return {
id: message.id,
@@ -87,6 +90,7 @@ export function buildDmMessageWithUser(
thumbnailFilename: a.thumbnailFilename ?? null,
createdAt: a.createdAt,
})),
embeds: embedRows.map(e => embedRowToEmbed(e)),
reactions,
replyTo,
};
@@ -108,6 +112,11 @@ export function getDmMessageWithUser(dmMessageId: string): DmMessageWithUser | n
.where(eq(schema.attachments.dmMessageId, dmMessageId))
.all();
const embedRows = db.select()
.from(schema.embeds)
.where(eq(schema.embeds.dmMessageId, dmMessageId))
.all();
const reactionsMap = fetchDmReactionsForMessages([dmMessageId]);
const reactions = reactionsMap.get(dmMessageId) ?? [];
@@ -127,13 +136,14 @@ export function getDmMessageWithUser(dmMessageId: string): DmMessageWithUser | n
createdAt: replyMsg.createdAt,
user: sanitizeUser(replyUser),
attachments: [],
embeds: [],
reactions: [],
};
}
}
}
return buildDmMessageWithUser(message, user, attachmentRows, reactions, replyTo);
return buildDmMessageWithUser(message, user, attachmentRows, reactions, replyTo, embedRows);
}
/**
@@ -830,6 +840,9 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
// Batch fetch reactions
const reactionsMap = fetchDmReactionsForMessages(messageIds);
// Batch fetch embeds
const embedMap = fetchDmEmbedsForMessages(messageIds);
// Batch fetch reply-to messages
const replyToIds = messageRows
.map(m => m.replyToId)
@@ -860,6 +873,7 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
createdAt: rm.createdAt,
user: sanitizeUser(rUser),
attachments: [],
embeds: [],
reactions: [],
});
}
@@ -871,7 +885,7 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is DmMessageWithUser => m !== null);
@@ -953,6 +967,11 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
// Broadcast to all DM members (including those who closed the channel)
broadcastDmMessage(id, message);
// Resolve embeds asynchronously after responding
setImmediate(() => {
resolveEmbeds(messageId, content?.trim() || null, id, true, null).catch(() => {});
});
return reply.code(201).send(message);
});
@@ -1006,6 +1025,11 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
});
}
// Re-resolve embeds asynchronously after responding
setImmediate(() => {
reResolveEmbeds(id, content.trim(), msg.dmChannelId, true, null).catch(() => {});
});
return reply.code(200).send(updated);
});
+23 -3
View File
@@ -12,9 +12,11 @@ import {
type PaginatedQuery,
type MessageWithUser,
type Reaction,
type Embed,
} from '@backspace/shared';
import { sanitizeUser } from '../utils/sanitize.js';
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
import { fetchEmbedsForMessages, resolveEmbeds, reResolveEmbeds, embedRowToEmbed } from '../utils/embedResolver.js';
/**
* Fetch reactions for a set of message IDs.
@@ -114,6 +116,7 @@ export function fetchReplyToMessages(messages: (typeof schema.messages.$inferSel
size: a.size,
createdAt: a.createdAt,
})),
embeds: [],
reactions: [],
replyTo: null,
});
@@ -127,6 +130,7 @@ export function buildMessageWithUser(
attachmentRows: (typeof schema.attachments.$inferSelect)[],
reactions: Reaction[] = [],
replyTo: MessageWithUser | null = null,
embedRows: (typeof schema.embeds.$inferSelect)[] = [],
): MessageWithUser {
return {
id: message.id,
@@ -147,6 +151,7 @@ export function buildMessageWithUser(
thumbnailFilename: a.thumbnailFilename ?? null,
createdAt: a.createdAt,
})),
embeds: embedRows.map(e => embedRowToEmbed(e)),
reactions,
replyTo,
};
@@ -224,6 +229,9 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
// Batch fetch reactions for all messages
const reactionsMap = fetchReactionsForMessages(messageIds);
// Batch fetch embeds for all messages
const embedMap = fetchEmbedsForMessages(messageIds);
// Batch fetch reply-to messages
const replyToMap = fetchReplyToMessages(messageRows);
@@ -233,7 +241,7 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is MessageWithUser => m !== null);
@@ -348,6 +356,11 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
message: messageWithUser,
});
// Resolve embeds asynchronously after responding
setImmediate(() => {
resolveEmbeds(messageId, content?.trim() || null, id, false, spaceId).catch(() => {});
});
return reply.code(201).send(messageWithUser);
});
@@ -397,16 +410,18 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
.where(eq(schema.attachments.messageId, id))
.all();
// Hydrate reactions and reply-to
// Hydrate reactions, embeds, and reply-to
const reactionsMap = fetchReactionsForMessages([id]);
const reactions = reactionsMap.get(id) ?? [];
const embedMap = fetchEmbedsForMessages([id]);
const embedRows = embedMap.get(id) ?? [];
let replyTo: MessageWithUser | null = null;
if (updatedMessage.replyToId) {
const replyToMap = fetchReplyToMessages([updatedMessage]);
replyTo = replyToMap.get(updatedMessage.replyToId) ?? null;
}
const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo);
const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo, embedRows);
// Broadcast edit
const spaceId = getChannelSpaceId(message.channelId);
@@ -415,6 +430,11 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
type: 'message_updated',
message: messageWithUser,
});
// Re-resolve embeds asynchronously after responding
setImmediate(() => {
reResolveEmbeds(id, content.trim(), message.channelId, false, spaceId).catch(() => {});
});
}
return reply.code(200).send(messageWithUser);
+11 -4
View File
@@ -7,6 +7,7 @@ import { fetchReactionsForMessages, fetchReplyToMessages, buildMessageWithUser }
import { fetchDmReactionsForMessages, buildDmMessageWithUser } from './dm.js';
import { sanitizeUser } from '../utils/sanitize.js';
import type { MessageWithUser, DmMessageWithUser } from '@backspace/shared';
import { fetchEmbedsForMessages, fetchDmEmbedsForMessages } from '../utils/embedResolver.js';
interface SearchQuery {
q?: string;
@@ -143,6 +144,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
}
const reactionsMap = fetchReactionsForMessages(messageIds);
const embedMap = fetchEmbedsForMessages(messageIds);
const replyToMap = fetchReplyToMessages(messageRows);
const results: MessageWithUser[] = messageRows
@@ -151,7 +153,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is MessageWithUser => m !== null);
@@ -269,6 +271,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
}
const reactionsMap = fetchDmReactionsForMessages(messageIds);
const embedMap = fetchDmEmbedsForMessages(messageIds);
// Fetch reply-to messages for DMs
const replyToIds = messageRows
@@ -299,6 +302,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
createdAt: rm.createdAt,
user: sanitizeUser(rUser),
attachments: [],
embeds: [],
reactions: [],
});
}
@@ -310,7 +314,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is DmMessageWithUser => m !== null);
@@ -405,6 +409,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
}
const reactionsMap = fetchReactionsForMessages(msgIds);
const embedMap = fetchEmbedsForMessages(msgIds);
const replyToMap = fetchReplyToMessages(uniqueRows);
const messages: MessageWithUser[] = uniqueRows
@@ -413,7 +418,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is MessageWithUser => m !== null);
@@ -498,6 +503,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
}
const reactionsMap = fetchDmReactionsForMessages(msgIds);
const embedMap = fetchDmEmbedsForMessages(msgIds);
const replyToIds = uniqueRows
.map(m => m.replyToId)
@@ -527,6 +533,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
createdAt: rm.createdAt,
user: sanitizeUser(rUser),
attachments: [],
embeds: [],
reactions: [],
});
}
@@ -538,7 +545,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
if (!user) return null;
const reactions = reactionsMap.get(m.id) ?? [];
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
})
.filter((m): m is DmMessageWithUser => m !== null);