@@ -2,14 +2,34 @@ const { logger } = require('@librechat/data-schemas');
22const { createTempChatExpirationDate } = require ( '@librechat/api' ) ;
33const { getMessages, deleteMessages } = require ( './Message' ) ;
44const { Conversation } = require ( '~/db/models' ) ;
5+ const { isSolidUser } = require ( '~/server/utils/isSolidUser' ) ;
6+ const {
7+ getConvoFromSolid,
8+ saveConvoToSolid,
9+ getConvosByCursorFromSolid,
10+ deleteConvosFromSolid,
11+ } = require ( '~/server/services/SolidStorage' ) ;
512
613/**
714 * Searches for a conversation by conversationId and returns a lean document with only conversationId and user.
815 * @param {string } conversationId - The conversation's ID.
16+ * @param {Object } [req] - Optional request object for Solid storage support.
917 * @returns {Promise<{conversationId: string, user: string} | null> } The conversation object with selected fields or null if not found.
1018 */
11- const searchConversation = async ( conversationId ) => {
19+ const searchConversation = async ( conversationId , req = null ) => {
1220 try {
21+ // Solid users: use Solid storage only; no MongoDB fallback
22+ if ( isSolidUser ( req ) ) {
23+ const convo = await getConvoFromSolid ( req , conversationId ) ;
24+ if ( convo ) {
25+ return {
26+ conversationId : convo . conversationId ,
27+ user : convo . user ,
28+ } ;
29+ }
30+ return null ;
31+ }
32+
1333 return await Conversation . findOne ( { conversationId } , 'conversationId user' ) . lean ( ) ;
1434 } catch ( error ) {
1535 logger . error ( '[searchConversation] Error searching conversation' , error ) ;
@@ -21,9 +41,16 @@ const searchConversation = async (conversationId) => {
2141 * Retrieves a single conversation for a given user and conversation ID.
2242 * @param {string } user - The user's ID.
2343 * @param {string } conversationId - The conversation's ID.
44+ * @param {Object } [req] - Optional request object for Solid storage support.
2445 * @returns {Promise<TConversation> } The conversation object.
2546 */
26- const getConvo = async ( user , conversationId ) => {
47+ const getConvo = async ( user , conversationId , req = null ) => {
48+ // Solid users: use Solid storage only; no MongoDB fallback
49+ if ( isSolidUser ( req ) ) {
50+ const convo = await getConvoFromSolid ( req , conversationId ) ;
51+ return convo ?? null ;
52+ }
53+
2754 try {
2855 return await Conversation . findOne ( { user, conversationId } ) . lean ( ) ;
2956 } catch ( error ) {
@@ -87,31 +114,58 @@ module.exports = {
87114 * @returns {Promise<TConversation> } The conversation object.
88115 */
89116 saveConvo : async ( req , { conversationId, newConversationId, ...convo } , metadata ) => {
90- try {
91- if ( metadata ?. context ) {
92- logger . debug ( `[saveConvo] ${ metadata . context } ` ) ;
117+ if ( metadata ?. context ) {
118+ logger . debug ( `[saveConvo] ${ metadata . context } ` ) ;
119+ }
120+
121+ // Build shared payload: expiredAt and base convo fields
122+ let expiredAt = null ;
123+ if ( req ?. body ?. isTemporary ) {
124+ try {
125+ const appConfig = req . config ;
126+ expiredAt = createTempChatExpirationDate ( appConfig ?. interfaceConfig ) ;
127+ } catch ( err ) {
128+ logger . error ( 'Error creating temporary chat expiration date:' , err ) ;
129+ logger . info ( `---\`saveConvo\` context: ${ metadata ?. context } ` ) ;
93130 }
131+ }
132+ const baseConvo = {
133+ conversationId,
134+ newConversationId,
135+ ...convo ,
136+ expiredAt,
137+ } ;
94138
95- const messages = await getMessages ( { conversationId } , '_id' ) ;
96- const update = { ...convo , messages, user : req . user . id } ;
139+ if ( isSolidUser ( req ) ) {
140+ try {
141+ // Full document aligned with schema (same shape as MongoDB); SolidStorage adds messages + timestamps
142+ const finalConversationId = newConversationId || conversationId ;
143+ const convoDocument = {
144+ ...baseConvo ,
145+ conversationId : finalConversationId ,
146+ user : req . user . id ,
147+ } ;
148+ if ( newConversationId && newConversationId !== conversationId ) {
149+ convoDocument . previousConversationId = conversationId ;
150+ }
151+ const savedConvo = await saveConvoToSolid ( req , convoDocument , metadata ) ;
152+ return savedConvo ;
153+ } catch ( error ) {
154+ logger . error ( '[saveConvo] Error saving conversation to Solid Pod' , error ) ;
155+ if ( metadata && metadata ?. context ) {
156+ logger . info ( `[saveConvo] ${ metadata . context } ` ) ;
157+ }
158+ throw error ;
159+ }
160+ }
97161
162+ try {
163+ const messages = await getMessages ( { conversationId } , '_id' ) ;
164+ const update = { ...convo , messages, user : req . user . id , expiredAt } ;
98165 if ( newConversationId ) {
99166 update . conversationId = newConversationId ;
100167 }
101168
102- if ( req ?. body ?. isTemporary ) {
103- try {
104- const appConfig = req . config ;
105- update . expiredAt = createTempChatExpirationDate ( appConfig ?. interfaceConfig ) ;
106- } catch ( err ) {
107- logger . error ( 'Error creating temporary chat expiration date:' , err ) ;
108- logger . info ( `---\`saveConvo\` context: ${ metadata ?. context } ` ) ;
109- update . expiredAt = null ;
110- }
111- } else {
112- update . expiredAt = null ;
113- }
114-
115169 /** @type {{ $set: Partial<TConversation>; $unset?: Record<keyof TConversation, number> } } */
116170 const updateOperation = { $set : update } ;
117171 if ( metadata && metadata . unsetFields && Object . keys ( metadata . unsetFields ) . length > 0 ) {
@@ -122,10 +176,7 @@ module.exports = {
122176 const conversation = await Conversation . findOneAndUpdate (
123177 { conversationId, user : req . user . id } ,
124178 updateOperation ,
125- {
126- new : true ,
127- upsert : metadata ?. noUpsert !== true ,
128- } ,
179+ { new : true , upsert : metadata ?. noUpsert !== true } ,
129180 ) ;
130181
131182 if ( ! conversation ) {
@@ -170,8 +221,20 @@ module.exports = {
170221 search,
171222 sortBy = 'updatedAt' ,
172223 sortDirection = 'desc' ,
224+ req, // Optional req object for Solid storage
173225 } = { } ,
174226 ) => {
227+ if ( isSolidUser ( req ) ) {
228+ return await getConvosByCursorFromSolid ( req , {
229+ cursor,
230+ limit,
231+ isArchived,
232+ tags,
233+ search,
234+ sortBy,
235+ sortDirection,
236+ } ) ;
237+ }
175238 const filters = [ { user } ] ;
176239 if ( isArchived ) {
177240 filters . push ( { isArchived : true } ) ;
@@ -228,7 +291,7 @@ module.exports = {
228291 } ,
229292 ] ,
230293 } ;
231- } catch ( err ) {
294+ } catch ( _err ) {
232295 logger . warn ( '[getConvosByCursor] Invalid cursor format, starting from beginning' ) ;
233296 }
234297 if ( cursorFilter ) {
@@ -337,6 +400,7 @@ module.exports = {
337400 * @function
338401 * @param {string|ObjectId } user - The user's ID.
339402 * @param {Object } filter - Additional filter criteria for the conversations to be deleted.
403+ * @param {Object } [req] - Optional Express request object for Solid storage support.
340404 * @returns {Promise<{ n: number, ok: number, deletedCount: number, messages: { n: number, ok: number, deletedCount: number } }> }
341405 * An object containing the count of deleted conversations and associated messages.
342406 * @throws {Error } Throws an error if there's an issue with the database operations.
@@ -347,7 +411,71 @@ module.exports = {
347411 * const result = await deleteConvos(user, filter);
348412 * logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
349413 */
350- deleteConvos : async ( user , filter ) => {
414+ deleteConvos : async ( user , filter , req = null ) => {
415+ // Use Solid storage when user logged in via "Continue with Solid"
416+ if ( isSolidUser ( req ) ) {
417+ try {
418+ let conversationIds = [ ] ;
419+
420+ // If conversationId is specified in filter, use it directly
421+ if ( filter . conversationId ) {
422+ conversationIds = [ filter . conversationId ] ;
423+ } else {
424+ // Otherwise, get all conversations matching the filter from Solid Pod
425+ // For now, we'll get all conversations and filter in memory
426+ // This is not ideal for large datasets, but Solid Pod doesn't support complex queries
427+ const allConversations = await getConvosByCursorFromSolid ( req , {
428+ limit : 10000 , // Large limit to get all conversations
429+ isArchived : filter . isArchived ,
430+ } ) ;
431+
432+ // Filter conversations based on the filter criteria
433+ let filteredConversations = allConversations . conversations ;
434+
435+ if ( filter . conversationId ) {
436+ filteredConversations = filteredConversations . filter (
437+ ( c ) => c . conversationId === filter . conversationId ,
438+ ) ;
439+ }
440+
441+ if ( filter . endpoint ) {
442+ filteredConversations = filteredConversations . filter (
443+ ( c ) => c . endpoint === filter . endpoint ,
444+ ) ;
445+ }
446+
447+ conversationIds = filteredConversations . map ( ( c ) => c . conversationId ) ;
448+ }
449+
450+ if ( ! conversationIds . length ) {
451+ throw new Error ( 'Conversation not found or already deleted.' ) ;
452+ }
453+
454+ const deletedCount = await deleteConvosFromSolid ( req , conversationIds ) ;
455+
456+ // Return format similar to MongoDB deleteMany result
457+ return {
458+ n : deletedCount ,
459+ ok : deletedCount > 0 ? 1 : 0 ,
460+ deletedCount,
461+ messages : {
462+ n : deletedCount , // Messages are deleted as part of deleteConvosFromSolid
463+ ok : deletedCount > 0 ? 1 : 0 ,
464+ deletedCount,
465+ } ,
466+ } ;
467+ } catch ( error ) {
468+ logger . error ( '[deleteConvos] Error deleting conversations from Solid Pod' , {
469+ error : error . message ,
470+ stack : error . stack ,
471+ filter,
472+ userId : user ,
473+ } ) ;
474+ throw error ;
475+ }
476+ }
477+
478+ // MongoDB storage (original code)
351479 try {
352480 const userFilter = { ...filter , user } ;
353481 const conversations = await Conversation . find ( userFilter ) . select ( 'conversationId' ) ;
0 commit comments