mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 06:58:10 +01:00
* feat: replace unsupported MongoDB aggregation operators for FerretDB compatibility Replace $lookup, $unwind, $sample, $replaceRoot, and $addFields aggregation stages which are unsupported on FerretDB v2.x (postgres-documentdb backend). - Prompt.js: Replace $lookup/$unwind/$project pipelines with find().select().lean() + attachProductionPrompts() batch helper. Replace $group/$replaceRoot/$sample in getRandomPromptGroups with distinct() + Fisher-Yates shuffle. - Agent/Prompt migration scripts: Replace $lookup anti-join pattern with distinct() + $nin two-step queries for finding un-migrated resources. All replacement patterns verified against FerretDB v2.7.0. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: use $pullAll for simple array removals, fix memberIds type mismatches Replace $pull with $pullAll for exact-value scalar array removals. Both operators work on MongoDB and FerretDB, but $pullAll is more explicit for exact matching (no condition expressions). Fix critical type mismatch bugs where ObjectId values were used against String[] memberIds arrays in Group queries: - config/delete-user.js: use string uid instead of ObjectId user._id - e2e/setup/cleanupUser.ts: convert userId.toString() before query Harden PermissionService.bulkUpdateResourcePermissions abort handling to prevent crash when abortTransaction is called after commitTransaction. All changes verified against FerretDB v2.7.0 and MongoDB Memory Server. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: harden transaction support probe for FerretDB compatibility Commit the transaction before aborting in supportsTransactions probe, and wrap abortTransaction in try-catch to prevent crashes when abort is called after a successful commit (observed behavior on FerretDB). Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add FerretDB compatibility test suite, retry utilities, and CI config Add comprehensive FerretDB integration test suite covering: - $pullAll scalar array operations - $pull with subdocument conditions - $lookup replacement (find + manual join) - $sample replacement (distinct + Fisher-Yates) - $bit and $bitsAllSet operations - Migration anti-join pattern - Multi-tenancy (useDb, scaling, write amplification) - Sharding proof-of-concept - Production operations (backup/restore, schema migration, deadlock retry) Add production retryWithBackoff utility for deadlock recovery during concurrent index creation on FerretDB/DocumentDB backends. Add UserController.spec.js tests for deleteUserController (runs in CI). Configure jest and eslint to isolate FerretDB tests from CI pipelines: - packages/data-schemas/jest.config.mjs: ignore misc/ directory - eslint.config.mjs: ignore packages/data-schemas/misc/ Include Docker Compose config for local FerretDB v2.7 + postgres-documentdb, dedicated jest/tsconfig for the test files, and multi-tenancy findings doc. Co-authored-by: Cursor <cursoragent@cursor.com> * style: brace formatting in aclEntry.ts modifyPermissionBits Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: reorganize retry utilities and update imports - Moved retryWithBackoff utility to a new file `retry.ts` for better structure. - Updated imports in `orgOperations.ferretdb.spec.ts` to reflect the new location of retry utilities. - Removed old import statement for retryWithBackoff from index.ts to streamline exports. * test: add $pullAll coverage for ConversationTag and PermissionService Add integration tests for deleteConversationTag verifying $pullAll removes tags from conversations correctly, and for syncUserEntraGroupMemberships verifying $pullAll removes user from non-matching Entra groups while preserving local group membership. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.4 KiB
JavaScript
128 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-nocheck
|
|
const path = require('path');
|
|
const mongoose = require('mongoose');
|
|
const {
|
|
Key,
|
|
User,
|
|
File,
|
|
Agent,
|
|
Token,
|
|
Group,
|
|
Action,
|
|
Preset,
|
|
Prompt,
|
|
Balance,
|
|
Message,
|
|
Session,
|
|
AclEntry,
|
|
ToolCall,
|
|
Assistant,
|
|
SharedLink,
|
|
PluginAuth,
|
|
MemoryEntry,
|
|
PromptGroup,
|
|
AgentApiKey,
|
|
Transaction,
|
|
Conversation,
|
|
ConversationTag,
|
|
} = require('@librechat/data-schemas').createModels(mongoose);
|
|
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
|
const { askQuestion, silentExit } = require('./helpers');
|
|
const connect = require('./connect');
|
|
|
|
async function gracefulExit(code = 0) {
|
|
try {
|
|
await mongoose.disconnect();
|
|
} catch (err) {
|
|
console.error('Error disconnecting from MongoDB:', err);
|
|
}
|
|
silentExit(code);
|
|
}
|
|
|
|
(async () => {
|
|
await connect();
|
|
|
|
console.purple('---------------');
|
|
console.purple('Deleting a user and all related data');
|
|
console.purple('---------------');
|
|
|
|
// 1) Get email
|
|
let email = process.argv[2]?.trim();
|
|
if (!email) {
|
|
email = (await askQuestion('Email:')).trim();
|
|
}
|
|
|
|
// 2) Find user
|
|
const user = await User.findOne({ email: email.toLowerCase() });
|
|
if (!user) {
|
|
console.yellow(`No user found with email "${email}"`);
|
|
return gracefulExit(0);
|
|
}
|
|
|
|
// 3) Confirm full deletion
|
|
const confirmAll = await askQuestion(
|
|
`Really delete user ${user.email} (${user._id}) and ALL their data? (y/N)`,
|
|
);
|
|
if (confirmAll.toLowerCase() !== 'y') {
|
|
console.yellow('Aborted.');
|
|
return gracefulExit(0);
|
|
}
|
|
|
|
// 4) Ask specifically about transactions
|
|
const confirmTx = await askQuestion('Also delete all transaction history for this user? (y/N)');
|
|
const deleteTx = confirmTx.toLowerCase() === 'y';
|
|
|
|
const uid = user._id.toString();
|
|
|
|
// 5) Build and run deletion tasks
|
|
const tasks = [
|
|
Action.deleteMany({ user: uid }),
|
|
Agent.deleteMany({ author: uid }),
|
|
AgentApiKey.deleteMany({ user: uid }),
|
|
Assistant.deleteMany({ user: uid }),
|
|
Balance.deleteMany({ user: uid }),
|
|
ConversationTag.deleteMany({ user: uid }),
|
|
Conversation.deleteMany({ user: uid }),
|
|
Message.deleteMany({ user: uid }),
|
|
File.deleteMany({ user: uid }),
|
|
Key.deleteMany({ userId: uid }),
|
|
MemoryEntry.deleteMany({ userId: uid }),
|
|
PluginAuth.deleteMany({ userId: uid }),
|
|
Prompt.deleteMany({ author: uid }),
|
|
PromptGroup.deleteMany({ author: uid }),
|
|
Preset.deleteMany({ user: uid }),
|
|
Session.deleteMany({ user: uid }),
|
|
SharedLink.deleteMany({ user: uid }),
|
|
ToolCall.deleteMany({ user: uid }),
|
|
Token.deleteMany({ userId: uid }),
|
|
AclEntry.deleteMany({ principalId: user._id }),
|
|
];
|
|
|
|
if (deleteTx) {
|
|
tasks.push(Transaction.deleteMany({ user: uid }));
|
|
}
|
|
|
|
await Promise.all(tasks);
|
|
|
|
// 6) Remove user from all groups
|
|
await Group.updateMany({ memberIds: uid }, { $pullAll: { memberIds: [uid] } });
|
|
|
|
// 7) Finally delete the user document itself
|
|
await User.deleteOne({ _id: uid });
|
|
|
|
console.green(`✔ Successfully deleted user ${email} and all associated data.`);
|
|
if (!deleteTx) {
|
|
console.yellow('⚠️ Transaction history was retained.');
|
|
}
|
|
|
|
return gracefulExit(0);
|
|
})().catch(async (err) => {
|
|
if (!err.message.includes('fetch failed')) {
|
|
console.error('There was an uncaught error:');
|
|
console.error(err);
|
|
await mongoose.disconnect();
|
|
process.exit(1);
|
|
}
|
|
});
|