📸 refactor: Refresh Shared Links With Latest Snapshot (#13095)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* fix: refresh shared links with latest target

* fix: validate shared link refresh payload
This commit is contained in:
Danny Avila 2026-05-13 19:38:28 -04:00 committed by GitHub
parent 7f58e4c2ed
commit ae75fb68a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 150 additions and 20 deletions

View file

@ -87,6 +87,7 @@ router.get('/link/:conversationId', requireJwtAuth, async (req, res) => {
return res.status(200).json({
success: share.success,
shareId: share.shareId,
targetMessageId: share.targetMessageId,
conversationId: req.params.conversationId,
});
} catch (error) {
@ -112,7 +113,12 @@ router.post('/:conversationId', requireJwtAuth, async (req, res) => {
router.patch('/:shareId', requireJwtAuth, async (req, res) => {
try {
const updatedShare = await updateSharedLink(req.user.id, req.params.shareId);
const { targetMessageId } = req.body ?? {};
if (targetMessageId !== undefined && typeof targetMessageId !== 'string') {
return res.status(400).json({ message: 'targetMessageId must be a string' });
}
const updatedShare = await updateSharedLink(req.user.id, req.params.shareId, targetMessageId);
if (updatedShare) {
res.status(200).json(updatedShare);
} else {

View file

@ -92,7 +92,7 @@ export default function SharedLinkButton({
if (!shareId) {
return;
}
const updateShare = await mutateAsync({ shareId });
const updateShare = await mutateAsync({ shareId, targetMessageId });
const newLink = generateShareLink(updateShare.shareId);
setSharedLink(newLink);
setAnnouncement(localize('com_ui_link_refreshed'));

View file

@ -176,17 +176,17 @@ export const useCreateSharedLinkMutation = (
};
export const useUpdateSharedLinkMutation = (
options?: t.MutationOptions<t.TUpdateShareLinkRequest, { shareId: string }>,
): UseMutationResult<t.TSharedLinkResponse, unknown, { shareId: string }, unknown> => {
options?: t.MutationOptions<t.TUpdateShareLinkRequest, t.TUpdateShareLinkRequest>,
): UseMutationResult<t.TSharedLinkResponse, unknown, t.TUpdateShareLinkRequest, unknown> => {
const queryClient = useQueryClient();
const { onSuccess, ..._options } = options || {};
return useMutation(
({ shareId }) => {
({ shareId, targetMessageId }) => {
if (!shareId) {
throw new Error('Share ID is required');
}
return dataService.updateSharedLink(shareId);
return dataService.updateSharedLink(shareId, targetMessageId);
},
{
onSuccess: (_data: t.TSharedLinkResponse, vars, context) => {

View file

@ -83,8 +83,11 @@ export function createSharedLink(
return request.post(endpoints.createSharedLink(conversationId), { targetMessageId });
}
export function updateSharedLink(shareId: string): Promise<t.TSharedLinkResponse> {
return request.patch(endpoints.updateSharedLink(shareId));
export function updateSharedLink(
shareId: string,
targetMessageId?: string,
): Promise<t.TSharedLinkResponse> {
return request.patch(endpoints.updateSharedLink(shareId), { targetMessageId });
}
export function deleteSharedLink(shareId: string): Promise<m.TDeleteSharedLinkResponse> {

View file

@ -51,10 +51,7 @@ export const useGetSharedLinkQuery = (
refetchOnReconnect: false,
refetchOnMount: false,
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.sharedLinks, conversationId], {
conversationId: data.conversationId,
shareId: data.shareId,
});
queryClient.setQueryData([QueryKeys.sharedLinks, conversationId], data);
},
...config,
},

View file

@ -1024,6 +1024,7 @@ export type TConversation = z.infer<typeof tConversationSchema> & {
export const tSharedLinkSchema = z.object({
conversationId: z.string(),
shareId: z.string(),
targetMessageId: z.string().optional(),
messages: z.array(z.string()),
isPublic: z.boolean(),
title: z.string(),

View file

@ -313,12 +313,14 @@ export type TSharedMessagesResponse = Omit<TSharedLink, 'messages'> & {
export type TCreateShareLinkRequest = Pick<TConversation, 'conversationId'>;
export type TUpdateShareLinkRequest = Pick<TSharedLink, 'shareId'>;
export type TUpdateShareLinkRequest = Pick<TSharedLink, 'shareId' | 'targetMessageId'>;
export type TSharedLinkResponse = Pick<TSharedLink, 'shareId'> &
Pick<TSharedLink, 'targetMessageId'> &
Pick<TConversation, 'conversationId'>;
export type TSharedLinkGetResponse = TSharedLinkResponse & {
export type TSharedLinkGetResponse = Omit<TSharedLinkResponse, 'shareId'> & {
shareId: string | null;
success: boolean;
};

View file

@ -26,6 +26,7 @@ describe('Share Methods', () => {
user: { type: String, index: true },
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
shareId: { type: String, index: true },
targetMessageId: { type: String, required: false, index: true },
isPublic: { type: Boolean, default: true },
},
{ timestamps: true },
@ -714,6 +715,108 @@ describe('Share Methods', () => {
);
});
test('should update branch target to the latest refreshed message', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const rootMessageId = `msg_${nanoid()}`;
const oldAnswerId = `msg_${nanoid()}`;
const rerunPromptId = `msg_${nanoid()}`;
const rerunAnswerId = `msg_${nanoid()}`;
await Conversation.create({
conversationId,
title: 'Analysis Conversation',
user: userId,
});
const initialMessages = await Message.create([
{
messageId: rootMessageId,
conversationId,
user: userId,
text: 'Analyze February 2023 to October 2025',
isCreatedByUser: true,
parentMessageId: Constants.NO_PARENT,
},
{
messageId: oldAnswerId,
conversationId,
user: userId,
text: 'Old analysis result',
isCreatedByUser: false,
parentMessageId: rootMessageId,
},
]);
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: initialMessages.map((message) => message._id),
targetMessageId: oldAnswerId,
isPublic: true,
});
await Message.create([
{
messageId: rerunPromptId,
conversationId,
user: userId,
text: 'Rerun for March 2023 to January 2026',
isCreatedByUser: true,
parentMessageId: oldAnswerId,
},
{
messageId: rerunAnswerId,
conversationId,
user: userId,
text: 'Updated analysis result',
isCreatedByUser: false,
parentMessageId: rerunPromptId,
},
]);
const result = await shareMethods.updateSharedLink(userId, shareId, rerunAnswerId);
const updatedShare = await SharedLink.findOne({ shareId: result.shareId }).populate(
'messages',
);
const sharedMessages = await shareMethods.getSharedMessages(result.shareId);
expect(result.shareId).not.toBe(shareId);
expect(result.targetMessageId).toBe(rerunAnswerId);
expect(updatedShare?.targetMessageId).toBe(rerunAnswerId);
expect(updatedShare?.messages).toHaveLength(4);
expect(sharedMessages?.messages.map((message) => message.text)).toEqual([
'Analyze February 2023 to October 2025',
'Old analysis result',
'Rerun for March 2023 to January 2026',
'Updated analysis result',
]);
});
test('should preserve existing branch target when refresh has no target override', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const targetMessageId = `msg_${nanoid()}`;
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: [],
targetMessageId,
isPublic: true,
});
const result = await shareMethods.updateSharedLink(userId, shareId);
const updatedShare = await SharedLink.findOne({ shareId: result.shareId });
expect(result.targetMessageId).toBe(targetMessageId);
expect(updatedShare?.targetMessageId).toBe(targetMessageId);
});
test('should not allow user to update shared link they do not own', async () => {
const ownerUserId = new mongoose.Types.ObjectId().toString();
const otherUserId = new mongoose.Types.ObjectId().toString();

View file

@ -410,7 +410,7 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
...(targetMessageId && { targetMessageId }),
});
return { shareId, conversationId };
return { shareId, conversationId, targetMessageId };
} catch (error) {
if (error instanceof ShareServiceError) {
throw error;
@ -439,14 +439,19 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
try {
const SharedLink = mongoose.models.SharedLink as Model<t.ISharedLink>;
const share = (await SharedLink.findOne({ conversationId, user, isPublic: true })
.select('shareId -_id')
.lean()) as { shareId?: string } | null;
.select('shareId targetMessageId -_id')
.sort({ updatedAt: -1 })
.lean()) as { shareId?: string; targetMessageId?: string } | null;
if (!share) {
return { shareId: null, success: false };
}
return { shareId: share.shareId || null, success: true };
return {
shareId: share.shareId || null,
targetMessageId: share.targetMessageId,
success: true,
};
} catch (error) {
logger.error('[getSharedLink] Error getting shared link', {
error: error instanceof Error ? error.message : 'Unknown error',
@ -460,7 +465,11 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
/**
* Update a shared link with new messages
*/
async function updateSharedLink(user: string, shareId: string): Promise<t.UpdateShareResult> {
async function updateSharedLink(
user: string,
shareId: string,
targetMessageId?: string,
): Promise<t.UpdateShareResult> {
if (!user || !shareId) {
throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS');
}
@ -481,10 +490,12 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
.lean();
const newShareId = nanoid();
const resolvedTargetMessageId = targetMessageId ?? share.targetMessageId;
const update = {
messages: updatedMessages,
user,
shareId: newShareId,
...(resolvedTargetMessageId && { targetMessageId: resolvedTargetMessageId }),
};
const updatedShare = (await SharedLink.findOneAndUpdate({ shareId, user }, update, {
@ -499,7 +510,11 @@ export function createShareMethods(mongoose: typeof import('mongoose')) {
anonymizeConvo(updatedShare);
return { shareId: newShareId, conversationId: updatedShare.conversationId };
return {
shareId: newShareId,
conversationId: updatedShare.conversationId,
targetMessageId: updatedShare.targetMessageId,
};
} catch (error) {
logger.error('[updateSharedLink] Error updating shared link', {
error: error instanceof Error ? error.message : 'Unknown error',

View file

@ -43,11 +43,13 @@ export interface SharedMessagesResult {
export interface CreateShareResult {
shareId: string;
conversationId: string;
targetMessageId?: string;
}
export interface UpdateShareResult {
shareId: string;
conversationId: string;
targetMessageId?: string;
}
export interface DeleteShareResult {
@ -58,6 +60,7 @@ export interface DeleteShareResult {
export interface GetShareLinkResult {
shareId: string | null;
targetMessageId?: string;
success: boolean;
}