🏗️ refactor: Extract DB layers to data-schemas for shared use (#7650)

* refactor: move model definitions and database-related methods to packages/data-schemas

* ci: update tests due to new DB structure

fix: disable mocking `librechat-data-provider`

feat: Add schema exports to data-schemas package

- Introduced a new schema module that exports various schemas including action, agent, and user schemas.
- Updated index.ts to include the new schema exports for better modularity and organization.

ci: fix appleStrategy tests

fix: Agent.spec.js

ci: refactor handleTools tests to use MongoMemoryServer for in-memory database

fix: getLogStores imports

ci: update banViolation tests to use MongoMemoryServer and improve session mocking

test: refactor samlStrategy tests to improve mock configurations and user handling

ci: fix crypto mock in handleText tests for improved accuracy

ci: refactor spendTokens tests to improve model imports and setup

ci: refactor Message model tests to use MongoMemoryServer and improve database interactions

* refactor: streamline IMessage interface and move feedback properties to types/message.ts

* refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen)

* refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity

* refactor: remove unused mongoose imports from model files for cleaner code

* refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code

* refactor: remove unused import in Transaction model for cleaner code

* ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch

* chore: cleanup imports
This commit is contained in:
Danny Avila 2025-05-30 22:18:13 -04:00 committed by GitHub
parent 4cbab86b45
commit a2fc7d312a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
161 changed files with 2998 additions and 2088 deletions

View file

@ -0,0 +1,264 @@
import type * as t from '~/types/session';
import { signPayload, hashToken } from '~/crypto';
import logger from '~/config/winston';
export class SessionError extends Error {
public code: string;
constructor(message: string, code: string = 'SESSION_ERROR') {
super(message);
this.name = 'SessionError';
this.code = code;
}
}
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
const expires = eval(REFRESH_TOKEN_EXPIRY ?? '0') ?? 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
*/
async function createSession(
userId: string,
options: t.CreateSessionOptions = {},
): Promise<t.SessionResult> {
if (!userId) {
throw new SessionError('User ID is required', 'INVALID_USER_ID');
}
try {
const session = new Session({
user: userId,
expiration: options.expiration || new Date(Date.now() + expires),
});
const refreshToken = await generateRefreshToken(session);
return { session, refreshToken };
} catch (error) {
logger.error('[createSession] Error creating session:', error);
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
}
}
/**
* Finds a session by various parameters
*/
async function findSession(
params: t.SessionSearchParams,
options: t.SessionQueryOptions = { lean: true },
): Promise<t.ISession | null> {
try {
const query: Record<string, unknown> = {};
if (!params.refreshToken && !params.userId && !params.sessionId) {
throw new SessionError(
'At least one search parameter is required',
'INVALID_SEARCH_PARAMS',
);
}
if (params.refreshToken) {
const tokenHash = await hashToken(params.refreshToken);
query.refreshTokenHash = tokenHash;
}
if (params.userId) {
query.user = params.userId;
}
if (params.sessionId) {
const sessionId =
typeof params.sessionId === 'object' &&
params.sessionId !== null &&
'sessionId' in params.sessionId
? (params.sessionId as { sessionId: string }).sessionId
: (params.sessionId as string);
if (!mongoose.Types.ObjectId.isValid(sessionId)) {
throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
}
query._id = sessionId;
}
// Add expiration check to only return valid sessions
query.expiration = { $gt: new Date() };
const sessionQuery = Session.findOne(query);
if (options.lean) {
return (await sessionQuery.lean()) as t.ISession | null;
}
return await sessionQuery.exec();
} catch (error) {
logger.error('[findSession] Error finding session:', error);
throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
}
}
/**
* Updates session expiration
*/
async function updateExpiration(
session: t.ISession | string,
newExpiration?: Date,
): Promise<t.ISession> {
try {
const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
if (!sessionDoc) {
throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
}
sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
return await sessionDoc.save();
} catch (error) {
logger.error('[updateExpiration] Error updating session:', error);
throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED');
}
}
/**
* Deletes a session by refresh token or session ID
*/
async function deleteSession(params: t.DeleteSessionParams): Promise<{ deletedCount?: number }> {
try {
if (!params.refreshToken && !params.sessionId) {
throw new SessionError(
'Either refreshToken or sessionId is required',
'INVALID_DELETE_PARAMS',
);
}
const query: Record<string, unknown> = {};
if (params.refreshToken) {
query.refreshTokenHash = await hashToken(params.refreshToken);
}
if (params.sessionId) {
query._id = params.sessionId;
}
const result = await Session.deleteOne(query);
if (result.deletedCount === 0) {
logger.warn('[deleteSession] No session found to delete');
}
return result;
} catch (error) {
logger.error('[deleteSession] Error deleting session:', error);
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
}
}
/**
* Deletes all sessions for a user
*/
async function deleteAllUserSessions(
userId: string | { userId: string },
options: t.DeleteAllSessionsOptions = {},
): Promise<{ deletedCount?: number }> {
try {
if (!userId) {
throw new SessionError('User ID is required', 'INVALID_USER_ID');
}
const userIdString =
typeof userId === 'object' && userId !== null ? userId.userId : (userId as string);
if (!mongoose.Types.ObjectId.isValid(userIdString)) {
throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
}
const query: Record<string, unknown> = { user: userIdString };
if (options.excludeCurrentSession && options.currentSessionId) {
query._id = { $ne: options.currentSessionId };
}
const result = await Session.deleteMany(query);
if (result.deletedCount && result.deletedCount > 0) {
logger.debug(
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`,
);
}
return result;
} catch (error) {
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
}
}
/**
* Generates a refresh token for a session
*/
async function generateRefreshToken(session: t.ISession): Promise<string> {
if (!session || !session.user) {
throw new SessionError('Invalid session object', 'INVALID_SESSION');
}
try {
const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
if (!session.expiration) {
session.expiration = new Date(expiresIn);
}
const refreshToken = await signPayload({
payload: {
id: session.user,
sessionId: session._id,
},
secret: process.env.JWT_REFRESH_SECRET!,
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
});
session.refreshTokenHash = await hashToken(refreshToken);
await session.save();
return refreshToken;
} catch (error) {
logger.error('[generateRefreshToken] Error generating refresh token:', error);
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
}
}
/**
* Counts active sessions for a user
*/
async function countActiveSessions(userId: string): Promise<number> {
try {
if (!userId) {
throw new SessionError('User ID is required', 'INVALID_USER_ID');
}
return await Session.countDocuments({
user: userId,
expiration: { $gt: new Date() },
});
} catch (error) {
logger.error('[countActiveSessions] Error counting active sessions:', error);
throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
}
}
return {
findSession,
SessionError,
deleteSession,
createSession,
updateExpiration,
countActiveSessions,
generateRefreshToken,
deleteAllUserSessions,
};
}
export type SessionMethods = ReturnType<typeof createSessionMethods>;