🏗️ 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

@ -18,17 +18,13 @@ const getProfileDetails = ({ idToken, profile }) => {
const decoded = jwt.decode(idToken);
logger.debug(
`Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`,
);
logger.debug(`Decoded Apple JWT: ${JSON.stringify(decoded, null, 2)}`);
return {
email: decoded.email,
id: decoded.sub,
avatarUrl: null, // Apple does not provide an avatar URL
username: decoded.email
? decoded.email.split('@')[0].toLowerCase()
: `user_${decoded.sub}`,
username: decoded.email ? decoded.email.split('@')[0].toLowerCase() : `user_${decoded.sub}`,
name: decoded.name
? `${decoded.name.firstName} ${decoded.name.lastName}`
: profile.displayName || null,

View file

@ -1,22 +1,25 @@
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const { logger } = require('@librechat/data-schemas');
const { Strategy: AppleStrategy } = require('passport-apple');
const socialLogin = require('./socialLogin');
const User = require('~/models/User');
const { logger } = require('~/config');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { createSocialUser, handleExistingUser } = require('./process');
const { isEnabled } = require('~/server/utils');
const socialLogin = require('./socialLogin');
const { findUser } = require('~/models');
const { User } = require('~/db/models');
// Mocking external dependencies
jest.mock('jsonwebtoken');
jest.mock('~/config', () => ({
logger: {
error: jest.fn(),
debug: jest.fn(),
},
}));
jest.mock('@librechat/data-schemas', () => {
const actualModule = jest.requireActual('@librechat/data-schemas');
return {
...actualModule,
logger: {
error: jest.fn(),
debug: jest.fn(),
},
};
});
jest.mock('./process', () => ({
createSocialUser: jest.fn(),
handleExistingUser: jest.fn(),
@ -64,7 +67,6 @@ describe('Apple Login Strategy', () => {
// Define getProfileDetails within the test scope
getProfileDetails = ({ idToken, profile }) => {
console.log('getProfileDetails called with idToken:', idToken);
if (!idToken) {
logger.error('idToken is missing');
throw new Error('idToken is missing');
@ -84,9 +86,7 @@ describe('Apple Login Strategy', () => {
email: decoded.email,
id: decoded.sub,
avatarUrl: null, // Apple does not provide an avatar URL
username: decoded.email
? decoded.email.split('@')[0].toLowerCase()
: `user_${decoded.sub}`,
username: decoded.email ? decoded.email.split('@')[0].toLowerCase() : `user_${decoded.sub}`,
name: decoded.name
? `${decoded.name.firstName} ${decoded.name.lastName}`
: profile.displayName || null,
@ -96,8 +96,12 @@ describe('Apple Login Strategy', () => {
// Mock isEnabled based on environment variable
isEnabled.mockImplementation((flag) => {
if (flag === 'true') { return true; }
if (flag === 'false') { return false; }
if (flag === 'true') {
return true;
}
if (flag === 'false') {
return false;
}
return false;
});
@ -154,9 +158,7 @@ describe('Apple Login Strategy', () => {
});
expect(jwt.decode).toHaveBeenCalledWith('fake_id_token');
expect(logger.debug).toHaveBeenCalledWith(
expect.stringContaining('Decoded Apple JWT'),
);
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Decoded Apple JWT'));
expect(profileDetails).toEqual({
email: 'john.doe@example.com',
id: 'apple-sub-1234',
@ -209,7 +211,7 @@ describe('Apple Login Strategy', () => {
beforeEach(() => {
jwt.decode.mockReturnValue(decodedToken);
findUser.mockImplementation(({ email }) => User.findOne({ email }));
findUser.mockResolvedValue(null);
});
it('should create a new user if one does not exist and registration is allowed', async () => {
@ -248,7 +250,7 @@ describe('Apple Login Strategy', () => {
});
it('should handle existing user and update avatarUrl', async () => {
// Create an existing user
// Create an existing user without saving to database
const existingUser = new User({
email: 'jane.doe@example.com',
username: 'jane.doe',
@ -257,15 +259,15 @@ describe('Apple Login Strategy', () => {
providerId: 'apple-sub-9012',
avatarUrl: 'old_avatar.png',
});
await existingUser.save();
// Mock findUser to return the existing user
findUser.mockResolvedValue(existingUser);
// Mock handleExistingUser to update avatarUrl
// Mock handleExistingUser to update avatarUrl without saving to database
handleExistingUser.mockImplementation(async (user, avatarUrl) => {
user.avatarUrl = avatarUrl;
await user.save();
// Don't call save() to avoid database operations
return user;
});
const mockVerifyCallback = jest.fn();
@ -297,7 +299,7 @@ describe('Apple Login Strategy', () => {
appleStrategyInstance._verify(
fakeAccessToken,
fakeRefreshToken,
null, // idToken is missing
null, // idToken is missing
mockProfile,
(err, user) => {
mockVerifyCallback(err, user);

View file

@ -1,7 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { getUserById, updateUser } = require('~/models');
const { logger } = require('~/config');
// JWT strategy
const jwtLogin = () =>

View file

@ -1,10 +1,10 @@
const fs = require('fs');
const LdapStrategy = require('passport-ldapauth');
const { SystemRoles } = require('librechat-data-provider');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { countUsers } = require('~/models/userMethods');
const { logger } = require('@librechat/data-schemas');
const { createUser, findUser, updateUser, countUsers } = require('~/models');
const { getBalanceConfig } = require('~/server/services/Config');
const { isEnabled } = require('~/server/utils');
const logger = require('~/utils/logger');
const {
LDAP_URL,
@ -124,7 +124,8 @@ const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => {
name: fullName,
role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER,
};
const userId = await createUser(user);
const balanceConfig = await getBalanceConfig();
const userId = await createUser(user, balanceConfig);
user._id = userId;
} else {
// Users registered in LDAP are assumed to have their user information managed in LDAP,

View file

@ -1,9 +1,9 @@
const { logger } = require('@librechat/data-schemas');
const { errorsToString } = require('librechat-data-provider');
const { Strategy: PassportLocalStrategy } = require('passport-local');
const { findUser, comparePassword, updateUser } = require('~/models');
const { isEnabled, checkEmailConfig } = require('~/server/utils');
const { findUser, comparePassword, updateUser } = require('~/models');
const { loginSchema } = require('./validators');
const logger = require('~/utils/logger');
// Unix timestamp for 2024-06-07 15:20:18 Eastern Time
const verificationEnabledTimestamp = 1717788018;

View file

@ -1,16 +1,16 @@
const { CacheKeys } = require('librechat-data-provider');
const fetch = require('node-fetch');
const passport = require('passport');
const jwtDecode = require('jsonwebtoken/decode');
const { HttpsProxyAgent } = require('https-proxy-agent');
const client = require('openid-client');
const jwtDecode = require('jsonwebtoken/decode');
const { CacheKeys } = require('librechat-data-provider');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { hashToken, logger } = require('@librechat/data-schemas');
const { Strategy: OpenIDStrategy } = require('openid-client/passport');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { hashToken } = require('~/server/utils/crypto');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const { findUser, createUser, updateUser } = require('~/models');
const { getBalanceConfig } = require('~/server/services/Config');
const getLogStores = require('~/cache/getLogStores');
const { isEnabled } = require('~/server/utils');
/**
* @typedef {import('openid-client').ClientMetadata} ClientMetadata
@ -297,7 +297,10 @@ async function setupOpenId() {
emailVerified: userinfo.email_verified || false,
name: fullName,
};
user = await createUser(user, true, true);
const balanceConfig = await getBalanceConfig();
user = await createUser(user, balanceConfig, true, true);
} else {
user.provider = 'openid';
user.openidId = userinfo.sub;

View file

@ -1,7 +1,7 @@
const fetch = require('node-fetch');
const jwtDecode = require('jsonwebtoken/decode');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { setupOpenId } = require('./openidStrategy');
const { findUser, createUser, updateUser } = require('~/models');
// --- Mocks ---
jest.mock('node-fetch');
@ -11,7 +11,12 @@ jest.mock('~/server/services/Files/strategies', () => ({
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
})),
}));
jest.mock('~/models/userMethods', () => ({
jest.mock('~/server/services/Config', () => ({
getBalanceConfig: jest.fn(() => ({
enabled: false,
})),
}));
jest.mock('~/models', () => ({
findUser: jest.fn(),
createUser: jest.fn(),
updateUser: jest.fn(),
@ -36,11 +41,6 @@ jest.mock('~/cache/getLogStores', () =>
set: jest.fn(),
})),
);
jest.mock('librechat-data-provider', () => ({
CacheKeys: {
OPENID_EXCHANGED_TOKENS: 'openid-exchanged-tokens',
},
}));
// Mock the openid-client module and all its dependencies
jest.mock('openid-client', () => {
@ -174,6 +174,7 @@ describe('setupOpenId', () => {
email: userinfo.email,
name: `${userinfo.given_name} ${userinfo.family_name}`,
}),
{ enabled: false },
true,
true,
);
@ -193,6 +194,7 @@ describe('setupOpenId', () => {
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
{ enabled: false },
true,
true,
);
@ -212,6 +214,7 @@ describe('setupOpenId', () => {
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
{ enabled: false },
true,
true,
);
@ -229,6 +232,7 @@ describe('setupOpenId', () => {
expect(user.username).toBe(userinfo.sub);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: userinfo.sub }),
{ enabled: false },
true,
true,
);

View file

@ -1,7 +1,8 @@
const { FileSources } = require('librechat-data-provider');
const { createUser, updateUser, getUserById } = require('~/models/userMethods');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { updateUser, createUser, getUserById } = require('~/models');
const { getBalanceConfig } = require('~/server/services/Config');
/**
* Updates the avatar URL of an existing user. If the user's avatar URL does not include the query parameter
@ -78,7 +79,8 @@ const createSocialUser = async ({
emailVerified,
};
const newUserId = await createUser(update);
const balanceConfig = await getBalanceConfig();
const newUserId = await createUser(update, balanceConfig);
const fileStrategy = process.env.CDN_PROVIDER;
const isLocal = fileStrategy === FileSources.local;

View file

@ -2,11 +2,11 @@ const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const passport = require('passport');
const { hashToken, logger } = require('@librechat/data-schemas');
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { hashToken } = require('~/server/utils/crypto');
const { logger } = require('~/config');
const { findUser, createUser, updateUser } = require('~/models');
const { getBalanceConfig } = require('~/server/services/Config');
const paths = require('~/config/paths');
let crypto;
@ -218,7 +218,8 @@ async function setupSaml() {
emailVerified: true,
name: fullName,
};
user = await createUser(user, true, true);
const balanceConfig = await getBalanceConfig();
user = await createUser(user, balanceConfig, true, true);
} else {
user.provider = 'saml';
user.samlId = profile.nameID;

View file

@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { Strategy: SamlStrategy } = require('@node-saml/passport-saml');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { findUser, createUser, updateUser } = require('~/models');
const { setupSaml, getCertificateContent } = require('./samlStrategy');
// --- Mocks ---
@ -10,11 +10,29 @@ jest.mock('fs');
jest.mock('path');
jest.mock('node-fetch');
jest.mock('@node-saml/passport-saml');
jest.mock('~/models/userMethods', () => ({
jest.mock('~/models', () => ({
findUser: jest.fn(),
createUser: jest.fn(),
updateUser: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
config: {
registration: {
socialLogins: ['saml'],
},
},
getBalanceConfig: jest.fn().mockResolvedValue({
tokenCredits: 1000,
startingBalance: 1000,
}),
}));
jest.mock('~/server/services/Config/EndpointService', () => ({
config: {},
}));
jest.mock('~/server/utils', () => ({
isEnabled: jest.fn(() => false),
isUserProvided: jest.fn(() => false),
}));
jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(() => ({
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
@ -23,9 +41,6 @@ jest.mock('~/server/services/Files/strategies', () => ({
jest.mock('~/server/utils/crypto', () => ({
hashToken: jest.fn().mockResolvedValue('hashed-token'),
}));
jest.mock('~/server/utils', () => ({
isEnabled: jest.fn(() => false),
}));
jest.mock('~/config', () => ({
logger: {
info: jest.fn(),
@ -196,6 +211,18 @@ describe('setupSaml', () => {
beforeEach(async () => {
jest.clearAllMocks();
// Configure mocks
const { findUser, createUser, updateUser } = require('~/models');
findUser.mockResolvedValue(null);
createUser.mockImplementation(async (userData) => ({
_id: 'mock-user-id',
...userData,
}));
updateUser.mockImplementation(async (id, userData) => ({
_id: id,
...userData,
}));
const cert = `
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUKhXaFJGJJPx466rlwYORIsqCq7MwDQYJKoZIhvcNAQEL
@ -232,16 +259,6 @@ u7wlOSk+oFzDIO/UILIA
delete process.env.SAML_PICTURE_CLAIM;
delete process.env.SAML_NAME_CLAIM;
findUser.mockResolvedValue(null);
createUser.mockImplementation(async (userData) => ({
_id: 'newUserId',
...userData,
}));
updateUser.mockImplementation(async (id, userData) => ({
_id: id,
...userData,
}));
// Simulate image download
const fakeBuffer = Buffer.from('fake image');
fetch.mockResolvedValue({
@ -257,17 +274,10 @@ u7wlOSk+oFzDIO/UILIA
const { user } = await validate(profile);
expect(user.username).toBe(profile.username);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({
provider: 'saml',
samlId: profile.nameID,
username: profile.username,
email: profile.email,
name: `${profile.given_name} ${profile.family_name}`,
}),
true,
true,
);
expect(user.provider).toBe('saml');
expect(user.samlId).toBe(profile.nameID);
expect(user.email).toBe(profile.email);
expect(user.name).toBe(`${profile.given_name} ${profile.family_name}`);
});
it('should use given_name as username when username claim is missing', async () => {
@ -278,11 +288,7 @@ u7wlOSk+oFzDIO/UILIA
const { user } = await validate(profile);
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
true,
true,
);
expect(user.provider).toBe('saml');
});
it('should use email as username when username and given_name are missing', async () => {
@ -294,11 +300,7 @@ u7wlOSk+oFzDIO/UILIA
const { user } = await validate(profile);
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
true,
true,
);
expect(user.provider).toBe('saml');
});
it('should override username with SAML_USERNAME_CLAIM when set', async () => {
@ -308,11 +310,7 @@ u7wlOSk+oFzDIO/UILIA
const { user } = await validate(profile);
expect(user.username).toBe(profile.nameID);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: profile.nameID }),
true,
true,
);
expect(user.provider).toBe('saml');
});
it('should set the full name correctly when given_name and family_name exist', async () => {
@ -378,34 +376,26 @@ u7wlOSk+oFzDIO/UILIA
});
it('should update an existing user on login', async () => {
// Set up findUser to return an existing user
const { findUser } = require('~/models');
const existingUser = {
_id: 'existingUserId',
_id: 'existing-user-id',
provider: 'local',
email: baseProfile.email,
samlId: '',
username: '',
name: '',
username: 'oldusername',
name: 'Old Name',
};
findUser.mockImplementation(async (query) => {
if (query.samlId === baseProfile.nameID || query.email === baseProfile.email) {
return existingUser;
}
return null;
});
findUser.mockResolvedValue(existingUser);
const profile = { ...baseProfile };
await validate(profile);
const { user } = await validate(profile);
expect(updateUser).toHaveBeenCalledWith(
existingUser._id,
expect.objectContaining({
provider: 'saml',
samlId: baseProfile.nameID,
username: baseProfile.username,
name: `${baseProfile.given_name} ${baseProfile.family_name}`,
}),
);
expect(user.provider).toBe('saml');
expect(user.samlId).toBe(baseProfile.nameID);
expect(user.username).toBe(baseProfile.username);
expect(user.name).toBe(`${baseProfile.given_name} ${baseProfile.family_name}`);
expect(user.email).toBe(baseProfile.email);
});
it('should attempt to download and save the avatar if picture is provided', async () => {

View file

@ -1,7 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { createSocialUser, handleExistingUser } = require('./process');
const { isEnabled } = require('~/server/utils');
const { findUser } = require('~/models');
const { logger } = require('~/config');
const socialLogin =
(provider, getProfileDetails) => async (accessToken, refreshToken, idToken, profile, cb) => {