mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🕐 feat: Configurable Retention Period for Temporary Chats (#8056)
* feat: Add configurable retention period for temporary chats * Addressing eslint errors * Fix: failing test due to missing registration * Update: variable name and use hours instead of days for chat retention * Addressing comments * chore: fix import order in Conversation.js * chore: import order in Message.js * chore: fix import order in config.ts * chore: move common methods to packages/api to reduce potential for circular dependencies * refactor: update temp chat retention config type to Partial<TCustomConfig> * refactor: remove unused config variable from AppService and update loadCustomConfig tests with logger mock * refactor: handle model undefined edge case by moving Session model initialization inside methods --------- Co-authored-by: Rakshit Tiwari <rak1729e@gmail.com>
This commit is contained in:
parent
3ab1bd65e5
commit
cbda3cb529
35 changed files with 372 additions and 135 deletions
3
api/cache/banViolation.js
vendored
3
api/cache/banViolation.js
vendored
|
@ -1,7 +1,8 @@
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { isEnabled, math } = require('@librechat/api');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { isEnabled, math, removePorts } = require('~/server/utils');
|
||||
const { deleteAllUserSessions } = require('~/models');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const getLogStores = require('./getLogStores');
|
||||
|
||||
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
|
||||
|
|
2
api/cache/getLogStores.js
vendored
2
api/cache/getLogStores.js
vendored
|
@ -1,7 +1,7 @@
|
|||
const { Keyv } = require('keyv');
|
||||
const { isEnabled, math } = require('@librechat/api');
|
||||
const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider');
|
||||
const { logFile, violationFile } = require('./keyvFiles');
|
||||
const { isEnabled, math } = require('~/server/utils');
|
||||
const keyvRedis = require('./keyvRedis');
|
||||
const keyvMongo = require('./keyvMongo');
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const { createTempChatExpirationDate } = require('@librechat/api');
|
||||
const getCustomConfig = require('~/server/services/Config/loadCustomConfig');
|
||||
const { getMessages, deleteMessages } = require('./Message');
|
||||
const { Conversation } = require('~/db/models');
|
||||
|
||||
|
@ -98,10 +100,15 @@ module.exports = {
|
|||
update.conversationId = newConversationId;
|
||||
}
|
||||
|
||||
if (req.body.isTemporary) {
|
||||
const expiredAt = new Date();
|
||||
expiredAt.setDate(expiredAt.getDate() + 30);
|
||||
update.expiredAt = expiredAt;
|
||||
if (req?.body?.isTemporary) {
|
||||
try {
|
||||
const customConfig = await getCustomConfig();
|
||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
||||
} catch (err) {
|
||||
logger.error('Error creating temporary chat expiration date:', err);
|
||||
logger.info(`---\`saveConvo\` context: ${metadata?.context}`);
|
||||
update.expiredAt = null;
|
||||
}
|
||||
} else {
|
||||
update.expiredAt = null;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const { z } = require('zod');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { createTempChatExpirationDate } = require('@librechat/api');
|
||||
const getCustomConfig = require('~/server/services/Config/loadCustomConfig');
|
||||
const { Message } = require('~/db/models');
|
||||
|
||||
const idSchema = z.string().uuid();
|
||||
|
@ -54,9 +56,14 @@ async function saveMessage(req, params, metadata) {
|
|||
};
|
||||
|
||||
if (req?.body?.isTemporary) {
|
||||
const expiredAt = new Date();
|
||||
expiredAt.setDate(expiredAt.getDate() + 30);
|
||||
update.expiredAt = expiredAt;
|
||||
try {
|
||||
const customConfig = await getCustomConfig();
|
||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
||||
} catch (err) {
|
||||
logger.error('Error creating temporary chat expiration date:', err);
|
||||
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
||||
update.expiredAt = null;
|
||||
}
|
||||
} else {
|
||||
update.expiredAt = null;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
const cookies = require('cookie');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const openIdClient = require('openid-client');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
registerUser,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
requestPasswordReset,
|
||||
setOpenIDAuthTokens,
|
||||
resetPassword,
|
||||
setAuthTokens,
|
||||
registerUser,
|
||||
} = require('~/server/services/AuthService');
|
||||
const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models');
|
||||
const { getOpenIdConfig } = require('~/strategies');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { getResponseSender } = require('librechat-data-provider');
|
||||
const {
|
||||
handleAbortError,
|
||||
|
@ -10,9 +12,8 @@ const {
|
|||
clientRegistry,
|
||||
requestDataMap,
|
||||
} = require('~/server/cleanup');
|
||||
const { sendMessage, createOnProgress } = require('~/server/utils');
|
||||
const { createOnProgress } = require('~/server/utils');
|
||||
const { saveMessage } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const EditController = async (req, res, next, initializeClient) => {
|
||||
let {
|
||||
|
@ -198,7 +199,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
|||
const finalUserMessage = reqDataContext.userMessage;
|
||||
const finalResponseMessage = { ...response };
|
||||
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// errorHandler.js
|
||||
const { logger } = require('~/config');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, ViolationTypes } = require('librechat-data-provider');
|
||||
const { sendResponse } = require('~/server/middleware/error');
|
||||
const { recordUsage } = require('~/server/services/Threads');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const { sendResponse } = require('~/server/utils');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorHandlerContext
|
||||
|
@ -75,7 +75,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
|||
} else if (/Files.*are invalid/.test(error.message)) {
|
||||
const errorMessage = `Files are invalid, or may not have uploaded yet.${
|
||||
endpoint === 'azureAssistants'
|
||||
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
|
||||
? " If using Azure OpenAI, files are only available in the region of the assistant's model at the time of upload."
|
||||
: ''
|
||||
}`;
|
||||
return sendResponse(req, res, messageData, errorMessage);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { Constants } = require('librechat-data-provider');
|
||||
const {
|
||||
handleAbortError,
|
||||
|
@ -5,9 +7,7 @@ const {
|
|||
cleanupAbortController,
|
||||
} = require('~/server/middleware');
|
||||
const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup');
|
||||
const { sendMessage } = require('~/server/utils');
|
||||
const { saveMessage } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
let {
|
||||
|
@ -206,7 +206,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
|||
// Create a new response object with minimal copies
|
||||
const finalResponse = { ...response };
|
||||
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const { v4 } = require('uuid');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
Time,
|
||||
Constants,
|
||||
|
@ -19,20 +22,20 @@ const {
|
|||
addThreadMetadata,
|
||||
saveAssistantMessage,
|
||||
} = require('~/server/services/Threads');
|
||||
const { sendResponse, sendMessage, sleep, countTokens } = require('~/server/utils');
|
||||
const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService');
|
||||
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
||||
const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { createRunBody } = require('~/server/services/createRunBody');
|
||||
const { sendResponse } = require('~/server/middleware/error');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const { checkBalance } = require('~/models/balanceMethods');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { countTokens } = require('~/server/utils');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
|
@ -471,7 +474,7 @@ const chatV1 = async (req, res) => {
|
|||
await Promise.all(promises);
|
||||
|
||||
const sendInitialResponse = () => {
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
sync: true,
|
||||
conversationId,
|
||||
// messages: previousMessages,
|
||||
|
@ -587,7 +590,7 @@ const chatV1 = async (req, res) => {
|
|||
iconURL: endpointOption.iconURL,
|
||||
};
|
||||
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
requestMessage: {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const { v4 } = require('uuid');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
Time,
|
||||
Constants,
|
||||
|
@ -22,15 +25,14 @@ const { createErrorHandler } = require('~/server/controllers/assistants/errors')
|
|||
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||
const { sendMessage, sleep, countTokens } = require('~/server/utils');
|
||||
const { createRunBody } = require('~/server/services/createRunBody');
|
||||
const { getTransactions } = require('~/models/Transaction');
|
||||
const { checkBalance } = require('~/models/balanceMethods');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { countTokens } = require('~/server/utils');
|
||||
const { getModelMaxTokens } = require('~/utils');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* @route POST /
|
||||
|
@ -309,7 +311,7 @@ const chatV2 = async (req, res) => {
|
|||
await Promise.all(promises);
|
||||
|
||||
const sendInitialResponse = () => {
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
sync: true,
|
||||
conversationId,
|
||||
// messages: previousMessages,
|
||||
|
@ -432,7 +434,7 @@ const chatV2 = async (req, res) => {
|
|||
iconURL: endpointOption.iconURL,
|
||||
};
|
||||
|
||||
sendMessage(res, {
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
requestMessage: {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// errorHandler.js
|
||||
const { sendResponse } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, ViolationTypes, ContentTypes } = require('librechat-data-provider');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const { recordUsage, checkMessageGaps } = require('~/server/services/Threads');
|
||||
const { sendResponse } = require('~/server/middleware/error');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorHandlerContext
|
||||
|
@ -78,7 +78,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
|||
} else if (/Files.*are invalid/.test(error.message)) {
|
||||
const errorMessage = `Files are invalid, or may not have uploaded yet.${
|
||||
endpoint === 'azureAssistants'
|
||||
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
|
||||
? " If using Azure OpenAI, files are only available in the region of the assistant's model at the time of upload."
|
||||
: ''
|
||||
}`;
|
||||
return sendResponse(req, res, messageData, errorMessage);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
// abortMiddleware.js
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { countTokens, isEnabled, sendEvent } = require('@librechat/api');
|
||||
const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider');
|
||||
const { sendMessage, sendError, countTokens, isEnabled } = require('~/server/utils');
|
||||
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
const { sendError } = require('~/server/middleware/error');
|
||||
const { spendTokens } = require('~/models/spendTokens');
|
||||
const abortControllers = require('./abortControllers');
|
||||
const { saveMessage, getConvo } = require('~/models');
|
||||
const { abortRun } = require('./abortRun');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const abortDataMap = new WeakMap();
|
||||
|
||||
|
@ -101,7 +101,7 @@ async function abortMessage(req, res) {
|
|||
cleanupAbortController(abortKey);
|
||||
|
||||
if (res.headersSent && finalEvent) {
|
||||
return sendMessage(res, finalEvent);
|
||||
return sendEvent(res, finalEvent);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
@ -174,7 +174,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
|||
* @param {string} responseMessageId
|
||||
*/
|
||||
const onStart = (userMessage, responseMessageId) => {
|
||||
sendMessage(res, { message: userMessage, created: true });
|
||||
sendEvent(res, { message: userMessage, created: true });
|
||||
|
||||
const abortKey = userMessage?.conversationId ?? req.user.id;
|
||||
getReqData({ abortKey });
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||
const { checkMessageGaps, recordUsage } = require('~/server/services/Threads');
|
||||
const { deleteMessages } = require('~/models/Message');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { sendMessage } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const three_minutes = 1000 * 60 * 3;
|
||||
|
||||
|
@ -34,7 +34,7 @@ async function abortRun(req, res) {
|
|||
const [thread_id, run_id] = runValues.split(':');
|
||||
|
||||
if (!run_id) {
|
||||
logger.warn('[abortRun] Couldn\'t find run for cancel request', { thread_id });
|
||||
logger.warn("[abortRun] Couldn't find run for cancel request", { thread_id });
|
||||
return res.status(204).send({ message: 'Run not found' });
|
||||
} else if (run_id === 'cancelled') {
|
||||
logger.warn('[abortRun] Run already cancelled', { thread_id });
|
||||
|
@ -93,7 +93,7 @@ async function abortRun(req, res) {
|
|||
};
|
||||
|
||||
if (res.headersSent && finalEvent) {
|
||||
return sendMessage(res, finalEvent);
|
||||
return sendEvent(res, finalEvent);
|
||||
}
|
||||
|
||||
res.json(finalEvent);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const crypto = require('crypto');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { getResponseSender, Constants } = require('librechat-data-provider');
|
||||
const { sendMessage, sendError } = require('~/server/utils');
|
||||
const { sendError } = require('~/server/middleware/error');
|
||||
const { saveMessage } = require('~/models');
|
||||
|
||||
/**
|
||||
|
@ -36,7 +37,7 @@ const denyRequest = async (req, res, errorMessage) => {
|
|||
isCreatedByUser: true,
|
||||
text,
|
||||
};
|
||||
sendMessage(res, { message: userMessage, created: true });
|
||||
sendEvent(res, { message: userMessage, created: true });
|
||||
|
||||
const shouldSaveMessage = _convoId && parentMessageId && parentMessageId !== Constants.NO_PARENT;
|
||||
|
||||
|
|
|
@ -1,31 +1,9 @@
|
|||
const crypto = require('crypto');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { parseConvo } = require('librechat-data-provider');
|
||||
const { sendEvent, handleError } = require('@librechat/api');
|
||||
const { saveMessage, getMessages } = require('~/models/Message');
|
||||
const { getConvo } = require('~/models/Conversation');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Sends error data in Server Sent Events format and ends the response.
|
||||
* @param {object} res - The server response.
|
||||
* @param {string} message - The error message.
|
||||
*/
|
||||
const handleError = (res, message) => {
|
||||
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
res.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends message data in Server Sent Events format.
|
||||
* @param {Express.Response} res - - The server response.
|
||||
* @param {string | Object} message - The message to be sent.
|
||||
* @param {'message' | 'error' | 'cancel'} event - [Optional] The type of event. Default is 'message'.
|
||||
*/
|
||||
const sendMessage = (res, message, event = 'message') => {
|
||||
if (typeof message === 'string' && message.length === 0) {
|
||||
return;
|
||||
}
|
||||
res.write(`event: ${event}\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes an error with provided options, saves the error message and sends a corresponding SSE response
|
||||
|
@ -91,7 +69,7 @@ const sendError = async (req, res, options, callback) => {
|
|||
convo = parseConvo(errorMessage);
|
||||
}
|
||||
|
||||
return sendMessage(res, {
|
||||
return sendEvent(res, {
|
||||
final: true,
|
||||
requestMessage: query?.[0] ? query[0] : requestMessage,
|
||||
responseMessage: errorMessage,
|
||||
|
@ -120,12 +98,10 @@ const sendResponse = (req, res, data, errorMessage) => {
|
|||
if (errorMessage) {
|
||||
return sendError(req, res, { ...data, text: errorMessage });
|
||||
}
|
||||
return sendMessage(res, data);
|
||||
return sendEvent(res, data);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendResponse,
|
||||
handleError,
|
||||
sendMessage,
|
||||
sendError,
|
||||
sendResponse,
|
||||
};
|
|
@ -1,4 +1,7 @@
|
|||
const { klona } = require('klona');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
StepTypes,
|
||||
RunStatus,
|
||||
|
@ -11,11 +14,10 @@ const {
|
|||
} = require('librechat-data-provider');
|
||||
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
|
||||
const { processRequiredActions } = require('~/server/services/ToolService');
|
||||
const { createOnProgress, sendMessage, sleep } = require('~/server/utils');
|
||||
const { RunManager, waitForRun } = require('~/server/services/Runs');
|
||||
const { processMessages } = require('~/server/services/Threads');
|
||||
const { createOnProgress } = require('~/server/utils');
|
||||
const { TextStream } = require('~/app/clients');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Sorts, processes, and flattens messages to a single string.
|
||||
|
@ -64,7 +66,7 @@ async function createOnTextProgress({
|
|||
};
|
||||
|
||||
logger.debug('Content data:', contentData);
|
||||
sendMessage(openai.res, contentData);
|
||||
sendEvent(openai.res, contentData);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const { isUserProvided } = require('@librechat/api');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { isUserProvided, generateConfig } = require('~/server/utils');
|
||||
const { generateConfig } = require('~/server/utils/handleText');
|
||||
|
||||
const {
|
||||
OPENAI_API_KEY: openAIApiKey,
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
const path = require('path');
|
||||
const {
|
||||
CacheKeys,
|
||||
configSchema,
|
||||
EImageOutputType,
|
||||
validateSettingDefinitions,
|
||||
agentParamSettings,
|
||||
paramSettings,
|
||||
} = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const loadYaml = require('~/utils/loadYaml');
|
||||
const { logger } = require('~/config');
|
||||
const axios = require('axios');
|
||||
const yaml = require('js-yaml');
|
||||
const keyBy = require('lodash/keyBy');
|
||||
const { loadYaml } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
CacheKeys,
|
||||
configSchema,
|
||||
paramSettings,
|
||||
EImageOutputType,
|
||||
agentParamSettings,
|
||||
validateSettingDefinitions,
|
||||
} = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
jest.mock('axios');
|
||||
jest.mock('~/cache/getLogStores');
|
||||
jest.mock('~/utils/loadYaml');
|
||||
jest.mock('@librechat/api', () => ({
|
||||
...jest.requireActual('@librechat/api'),
|
||||
loadYaml: jest.fn(),
|
||||
}));
|
||||
jest.mock('librechat-data-provider', () => {
|
||||
const actual = jest.requireActual('librechat-data-provider');
|
||||
return {
|
||||
|
@ -30,11 +33,22 @@ jest.mock('librechat-data-provider', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
return {
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const axios = require('axios');
|
||||
const { loadYaml } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const loadCustomConfig = require('./loadCustomConfig');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const loadYaml = require('~/utils/loadYaml');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
describe('loadCustomConfig', () => {
|
||||
const mockSet = jest.fn();
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const { sleep } = require('@librechat/agents');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
Constants,
|
||||
StepTypes,
|
||||
|
@ -8,9 +11,8 @@ const {
|
|||
} = require('librechat-data-provider');
|
||||
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
|
||||
const { processRequiredActions } = require('~/server/services/ToolService');
|
||||
const { createOnProgress, sendMessage, sleep } = require('~/server/utils');
|
||||
const { processMessages } = require('~/server/services/Threads');
|
||||
const { logger } = require('~/config');
|
||||
const { createOnProgress } = require('~/server/utils');
|
||||
|
||||
/**
|
||||
* Implements the StreamRunManager functionality for managing the streaming
|
||||
|
@ -126,7 +128,7 @@ class StreamRunManager {
|
|||
conversationId: this.finalMessage.conversationId,
|
||||
};
|
||||
|
||||
sendMessage(this.res, contentData);
|
||||
sendEvent(this.res, contentData);
|
||||
}
|
||||
|
||||
/* <------------------ Misc. Helpers ------------------> */
|
||||
|
@ -302,7 +304,7 @@ class StreamRunManager {
|
|||
|
||||
for (const d of delta[key]) {
|
||||
if (typeof d === 'object' && !Object.prototype.hasOwnProperty.call(d, 'index')) {
|
||||
logger.warn('Expected an object with an \'index\' for array updates but got:', d);
|
||||
logger.warn("Expected an object with an 'index' for array updates but got:", d);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ const {
|
|||
defaultAssistantsVersion,
|
||||
defaultAgentCapabilities,
|
||||
} = require('librechat-data-provider');
|
||||
const { sendEvent } = require('@librechat/api');
|
||||
const { Providers } = require('@librechat/agents');
|
||||
const partialRight = require('lodash/partialRight');
|
||||
const { sendMessage } = require('./streamResponse');
|
||||
|
||||
/** Helper function to escape special characters in regex
|
||||
* @param {string} string - The string to escape.
|
||||
|
@ -37,7 +37,7 @@ const createOnProgress = (
|
|||
basePayload.text = basePayload.text + chunk;
|
||||
|
||||
const payload = Object.assign({}, basePayload, rest);
|
||||
sendMessage(res, payload);
|
||||
sendEvent(res, payload);
|
||||
if (_onProgress) {
|
||||
_onProgress(payload);
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ const createOnProgress = (
|
|||
const sendIntermediateMessage = (res, payload, extraTokens = '') => {
|
||||
basePayload.text = basePayload.text + extraTokens;
|
||||
const message = Object.assign({}, basePayload, payload);
|
||||
sendMessage(res, message);
|
||||
sendEvent(res, message);
|
||||
if (i === 0) {
|
||||
basePayload.initial = false;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
const streamResponse = require('./streamResponse');
|
||||
const removePorts = require('./removePorts');
|
||||
const countTokens = require('./countTokens');
|
||||
const handleText = require('./handleText');
|
||||
const sendEmail = require('./sendEmail');
|
||||
const queue = require('./queue');
|
||||
const files = require('./files');
|
||||
const math = require('./math');
|
||||
|
||||
/**
|
||||
* Check if email configuration is set
|
||||
|
@ -28,7 +26,6 @@ function checkEmailConfig() {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
...streamResponse,
|
||||
checkEmailConfig,
|
||||
...handleText,
|
||||
countTokens,
|
||||
|
@ -36,5 +33,4 @@ module.exports = {
|
|||
sendEmail,
|
||||
...files,
|
||||
...queue,
|
||||
math,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
const loadYaml = require('./loadYaml');
|
||||
const tokenHelpers = require('./tokens');
|
||||
const deriveBaseURL = require('./deriveBaseURL');
|
||||
const extractBaseURL = require('./extractBaseURL');
|
||||
const findMessageContent = require('./findMessageContent');
|
||||
|
||||
module.exports = {
|
||||
loadYaml,
|
||||
deriveBaseURL,
|
||||
extractBaseURL,
|
||||
...tokenHelpers,
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
function loadYaml(filepath) {
|
||||
try {
|
||||
let fileContents = fs.readFileSync(filepath, 'utf8');
|
||||
return yaml.load(fileContents);
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loadYaml;
|
|
@ -73,6 +73,8 @@ interface:
|
|||
bookmarks: true
|
||||
multiConvo: true
|
||||
agents: true
|
||||
# Temporary chat retention period in hours (default: 720, min: 1, max: 8760)
|
||||
# temporaryChatRetention: 1
|
||||
|
||||
# Example Cloudflare turnstile (optional)
|
||||
#turnstile:
|
||||
|
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -46573,6 +46573,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"eventsource": "^3.0.2",
|
||||
"express": "^4.21.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"keyv": "^5.3.2",
|
||||
"librechat-data-provider": "*",
|
||||
"node-fetch": "2.7.0",
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
"diff": "^7.0.0",
|
||||
"eventsource": "^3.0.2",
|
||||
"express": "^4.21.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"keyv": "^5.3.2",
|
||||
"librechat-data-provider": "*",
|
||||
"node-fetch": "2.7.0",
|
||||
|
|
|
@ -14,3 +14,13 @@ export function sendEvent(res: ServerResponse, event: ServerSentEvent): void {
|
|||
}
|
||||
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends error data in Server Sent Events format and ends the response.
|
||||
* @param res - The server response.
|
||||
* @param message - The error message.
|
||||
*/
|
||||
export function handleError(res: ServerResponse, message: string): void {
|
||||
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
|
|
|
@ -6,5 +6,8 @@ export * from './events';
|
|||
export * from './files';
|
||||
export * from './generators';
|
||||
export * from './llm';
|
||||
export * from './math';
|
||||
export * from './openid';
|
||||
export * from './tempChatRetention';
|
||||
export { default as Tokenizer } from './tokenizer';
|
||||
export * from './yaml';
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* If the input is not a string or contains invalid characters, an error is thrown.
|
||||
* If the evaluated result is not a number, an error is thrown.
|
||||
*
|
||||
* @param {string|number} str - The mathematical expression to evaluate, or a number.
|
||||
* @param {number} [fallbackValue] - The default value to return if the input is not a string or number, or if the evaluated result is not a number.
|
||||
* @param str - The mathematical expression to evaluate, or a number.
|
||||
* @param fallbackValue - The default value to return if the input is not a string or number, or if the evaluated result is not a number.
|
||||
*
|
||||
* @returns {number} The result of the evaluated expression or the input number.
|
||||
* @returns The result of the evaluated expression or the input number.
|
||||
*
|
||||
* @throws {Error} Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number.
|
||||
* @throws Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number.
|
||||
*/
|
||||
function math(str, fallbackValue) {
|
||||
export function math(str: string | number, fallbackValue?: number): number {
|
||||
const fallback = typeof fallbackValue !== 'undefined' && typeof fallbackValue === 'number';
|
||||
if (typeof str !== 'string' && typeof str === 'number') {
|
||||
return str;
|
||||
|
@ -43,5 +43,3 @@ function math(str, fallbackValue) {
|
|||
|
||||
return value;
|
||||
}
|
||||
|
||||
module.exports = math;
|
133
packages/api/src/utils/tempChatRetention.spec.ts
Normal file
133
packages/api/src/utils/tempChatRetention.spec.ts
Normal file
|
@ -0,0 +1,133 @@
|
|||
import {
|
||||
MIN_RETENTION_HOURS,
|
||||
MAX_RETENTION_HOURS,
|
||||
DEFAULT_RETENTION_HOURS,
|
||||
getTempChatRetentionHours,
|
||||
createTempChatExpirationDate,
|
||||
} from './tempChatRetention';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
describe('tempChatRetention', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.TEMP_CHAT_RETENTION_HOURS;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('getTempChatRetentionHours', () => {
|
||||
it('should return default retention hours when no config or env var is set', () => {
|
||||
const result = getTempChatRetentionHours();
|
||||
expect(result).toBe(DEFAULT_RETENTION_HOURS);
|
||||
});
|
||||
|
||||
it('should use environment variable when set', () => {
|
||||
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
|
||||
const result = getTempChatRetentionHours();
|
||||
expect(result).toBe(48);
|
||||
});
|
||||
|
||||
it('should use config value when set', () => {
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 12,
|
||||
},
|
||||
};
|
||||
const result = getTempChatRetentionHours(config);
|
||||
expect(result).toBe(12);
|
||||
});
|
||||
|
||||
it('should prioritize config over environment variable', () => {
|
||||
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 12,
|
||||
},
|
||||
};
|
||||
const result = getTempChatRetentionHours(config);
|
||||
expect(result).toBe(12);
|
||||
});
|
||||
|
||||
it('should enforce minimum retention period', () => {
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 0,
|
||||
},
|
||||
};
|
||||
const result = getTempChatRetentionHours(config);
|
||||
expect(result).toBe(MIN_RETENTION_HOURS);
|
||||
});
|
||||
|
||||
it('should enforce maximum retention period', () => {
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 10000,
|
||||
},
|
||||
};
|
||||
const result = getTempChatRetentionHours(config);
|
||||
expect(result).toBe(MAX_RETENTION_HOURS);
|
||||
});
|
||||
|
||||
it('should handle invalid environment variable', () => {
|
||||
process.env.TEMP_CHAT_RETENTION_HOURS = 'invalid';
|
||||
const result = getTempChatRetentionHours();
|
||||
expect(result).toBe(DEFAULT_RETENTION_HOURS);
|
||||
});
|
||||
|
||||
it('should handle invalid config value', () => {
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 'invalid' as unknown as number,
|
||||
},
|
||||
};
|
||||
const result = getTempChatRetentionHours(config);
|
||||
expect(result).toBe(DEFAULT_RETENTION_HOURS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTempChatExpirationDate', () => {
|
||||
it('should create expiration date with default retention period', () => {
|
||||
const result = createTempChatExpirationDate();
|
||||
|
||||
const expectedDate = new Date();
|
||||
expectedDate.setHours(expectedDate.getHours() + DEFAULT_RETENTION_HOURS);
|
||||
|
||||
// Allow for small time differences in test execution
|
||||
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
|
||||
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
|
||||
});
|
||||
|
||||
it('should create expiration date with custom retention period', () => {
|
||||
const config: Partial<TCustomConfig> = {
|
||||
interface: {
|
||||
temporaryChatRetention: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const result = createTempChatExpirationDate(config);
|
||||
|
||||
const expectedDate = new Date();
|
||||
expectedDate.setHours(expectedDate.getHours() + 12);
|
||||
|
||||
// Allow for small time differences in test execution
|
||||
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
|
||||
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
|
||||
});
|
||||
|
||||
it('should return a Date object', () => {
|
||||
const result = createTempChatExpirationDate();
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should return a future date', () => {
|
||||
const now = new Date();
|
||||
const result = createTempChatExpirationDate();
|
||||
expect(result.getTime()).toBeGreaterThan(now.getTime());
|
||||
});
|
||||
});
|
||||
});
|
77
packages/api/src/utils/tempChatRetention.ts
Normal file
77
packages/api/src/utils/tempChatRetention.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Default retention period for temporary chats in hours
|
||||
*/
|
||||
export const DEFAULT_RETENTION_HOURS = 24 * 30; // 30 days
|
||||
|
||||
/**
|
||||
* Minimum allowed retention period in hours
|
||||
*/
|
||||
export const MIN_RETENTION_HOURS = 1;
|
||||
|
||||
/**
|
||||
* Maximum allowed retention period in hours (1 year = 8760 hours)
|
||||
*/
|
||||
export const MAX_RETENTION_HOURS = 8760;
|
||||
|
||||
/**
|
||||
* Gets the temporary chat retention period from environment variables or config
|
||||
* @param config - The custom configuration object
|
||||
* @returns The retention period in hours
|
||||
*/
|
||||
export function getTempChatRetentionHours(config?: Partial<TCustomConfig> | null): number {
|
||||
let retentionHours = DEFAULT_RETENTION_HOURS;
|
||||
|
||||
// Check environment variable first
|
||||
if (process.env.TEMP_CHAT_RETENTION_HOURS) {
|
||||
const envValue = parseInt(process.env.TEMP_CHAT_RETENTION_HOURS, 10);
|
||||
if (!isNaN(envValue)) {
|
||||
retentionHours = envValue;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Invalid TEMP_CHAT_RETENTION_HOURS environment variable: ${process.env.TEMP_CHAT_RETENTION_HOURS}. Using default: ${DEFAULT_RETENTION_HOURS} hours.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check config file (takes precedence over environment variable)
|
||||
if (config?.interface?.temporaryChatRetention !== undefined) {
|
||||
const configValue = config.interface.temporaryChatRetention;
|
||||
if (typeof configValue === 'number' && !isNaN(configValue)) {
|
||||
retentionHours = configValue;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Invalid temporaryChatRetention in config: ${configValue}. Using ${retentionHours} hours.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the retention period
|
||||
if (retentionHours < MIN_RETENTION_HOURS) {
|
||||
logger.warn(
|
||||
`Temporary chat retention period ${retentionHours} is below minimum ${MIN_RETENTION_HOURS} hours. Using minimum value.`,
|
||||
);
|
||||
retentionHours = MIN_RETENTION_HOURS;
|
||||
} else if (retentionHours > MAX_RETENTION_HOURS) {
|
||||
logger.warn(
|
||||
`Temporary chat retention period ${retentionHours} exceeds maximum ${MAX_RETENTION_HOURS} hours. Using maximum value.`,
|
||||
);
|
||||
retentionHours = MAX_RETENTION_HOURS;
|
||||
}
|
||||
|
||||
return retentionHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expiration date for temporary chats
|
||||
* @param config - The custom configuration object
|
||||
* @returns The expiration date
|
||||
*/
|
||||
export function createTempChatExpirationDate(config?: Partial<TCustomConfig>): Date {
|
||||
const retentionHours = getTempChatRetentionHours(config);
|
||||
const expiredAt = new Date();
|
||||
expiredAt.setHours(expiredAt.getHours() + retentionHours);
|
||||
return expiredAt;
|
||||
}
|
11
packages/api/src/utils/yaml.ts
Normal file
11
packages/api/src/utils/yaml.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import fs from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export function loadYaml(filepath: string) {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(filepath, 'utf8');
|
||||
return yaml.load(fileContents);
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
|
@ -510,6 +510,7 @@ export const intefaceSchema = z
|
|||
prompts: z.boolean().optional(),
|
||||
agents: z.boolean().optional(),
|
||||
temporaryChat: z.boolean().optional(),
|
||||
temporaryChatRetention: z.number().min(1).max(8760).optional(),
|
||||
runCode: z.boolean().optional(),
|
||||
webSearch: z.boolean().optional(),
|
||||
})
|
||||
|
|
|
@ -13,14 +13,10 @@ export class SessionError extends Error {
|
|||
}
|
||||
|
||||
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||
const expires = REFRESH_TOKEN_EXPIRY
|
||||
? eval(REFRESH_TOKEN_EXPIRY)
|
||||
: 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
const expires = REFRESH_TOKEN_EXPIRY ? eval(REFRESH_TOKEN_EXPIRY) : 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
|
||||
// Factory function that takes mongoose instance and returns the methods
|
||||
export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
||||
const Session = mongoose.models.Session;
|
||||
|
||||
/**
|
||||
* Creates a new session for a user
|
||||
*/
|
||||
|
@ -33,13 +29,14 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
}
|
||||
|
||||
try {
|
||||
const session = new Session({
|
||||
const Session = mongoose.models.Session;
|
||||
const currentSession = new Session({
|
||||
user: userId,
|
||||
expiration: options.expiration || new Date(Date.now() + expires),
|
||||
});
|
||||
const refreshToken = await generateRefreshToken(session);
|
||||
const refreshToken = await generateRefreshToken(currentSession);
|
||||
|
||||
return { session, refreshToken };
|
||||
return { session: currentSession, refreshToken };
|
||||
} catch (error) {
|
||||
logger.error('[createSession] Error creating session:', error);
|
||||
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
|
||||
|
@ -54,6 +51,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
options: t.SessionQueryOptions = { lean: true },
|
||||
): Promise<t.ISession | null> {
|
||||
try {
|
||||
const Session = mongoose.models.Session;
|
||||
const query: Record<string, unknown> = {};
|
||||
|
||||
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
||||
|
@ -109,6 +107,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
newExpiration?: Date,
|
||||
): Promise<t.ISession> {
|
||||
try {
|
||||
const Session = mongoose.models.Session;
|
||||
const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
|
||||
|
||||
if (!sessionDoc) {
|
||||
|
@ -128,6 +127,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
*/
|
||||
async function deleteSession(params: t.DeleteSessionParams): Promise<{ deletedCount?: number }> {
|
||||
try {
|
||||
const Session = mongoose.models.Session;
|
||||
if (!params.refreshToken && !params.sessionId) {
|
||||
throw new SessionError(
|
||||
'Either refreshToken or sessionId is required',
|
||||
|
@ -166,6 +166,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
options: t.DeleteAllSessionsOptions = {},
|
||||
): Promise<{ deletedCount?: number }> {
|
||||
try {
|
||||
const Session = mongoose.models.Session;
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
}
|
||||
|
@ -237,6 +238,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) {
|
|||
*/
|
||||
async function countActiveSessions(userId: string): Promise<number> {
|
||||
try {
|
||||
const Session = mongoose.models.Session;
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue