mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
📸 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
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:
parent
7f58e4c2ed
commit
ae75fb68a6
10 changed files with 150 additions and 20 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue