| const path = require('path'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { ensureRequiredCollectionsExist } = require('@librechat/api'); |
| const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); |
| const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; |
|
|
| require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); |
| const connect = require('./connect'); |
|
|
| const { grantPermission } = require('~/server/services/PermissionService'); |
| const { getProjectByName } = require('~/models/Project'); |
| const { findRoleByIdentifier } = require('~/models'); |
| const { PromptGroup } = require('~/db/models'); |
|
|
| async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } = {}) { |
| await connect(); |
|
|
| logger.info('Starting PromptGroup Permissions Migration', { dryRun, batchSize }); |
|
|
| const mongoose = require('mongoose'); |
| |
| const db = mongoose.connection.db; |
| if (db) { |
| await ensureRequiredCollectionsExist(db); |
| } |
|
|
| |
| const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER); |
| const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER); |
| const editorRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR); |
|
|
| if (!ownerRole || !viewerRole || !editorRole) { |
| throw new Error('Required promptGroup roles not found. Run role seeding first.'); |
| } |
|
|
| |
| const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']); |
| const globalPromptGroupIds = new Set( |
| (globalProject?.promptGroupIds || []).map((id) => id.toString()), |
| ); |
|
|
| logger.info(`Found ${globalPromptGroupIds.size} prompt groups in global project`); |
|
|
| |
| const promptGroupsToMigrate = await PromptGroup.aggregate([ |
| { |
| $lookup: { |
| from: 'aclentries', |
| localField: '_id', |
| foreignField: 'resourceId', |
| as: 'aclEntries', |
| }, |
| }, |
| { |
| $addFields: { |
| promptGroupAclEntries: { |
| $filter: { |
| input: '$aclEntries', |
| as: 'aclEntry', |
| cond: { |
| $and: [ |
| { $eq: ['$$aclEntry.resourceType', ResourceType.PROMPTGROUP] }, |
| { $eq: ['$$aclEntry.principalType', PrincipalType.USER] }, |
| ], |
| }, |
| }, |
| }, |
| }, |
| }, |
| { |
| $match: { |
| author: { $exists: true, $ne: null }, |
| promptGroupAclEntries: { $size: 0 }, |
| }, |
| }, |
| { |
| $project: { |
| _id: 1, |
| name: 1, |
| author: 1, |
| authorName: 1, |
| category: 1, |
| }, |
| }, |
| ]); |
|
|
| const categories = { |
| globalViewAccess: [], |
| privateGroups: [], |
| }; |
|
|
| promptGroupsToMigrate.forEach((group) => { |
| const isGlobalGroup = globalPromptGroupIds.has(group._id.toString()); |
|
|
| if (isGlobalGroup) { |
| categories.globalViewAccess.push(group); |
| } else { |
| categories.privateGroups.push(group); |
| } |
| }); |
|
|
| logger.info( |
| 'PromptGroup categorization:\n' + |
| JSON.stringify( |
| { |
| globalViewAccess: categories.globalViewAccess.length, |
| privateGroups: categories.privateGroups.length, |
| total: promptGroupsToMigrate.length, |
| }, |
| null, |
| 2, |
| ), |
| ); |
|
|
| if (dryRun) { |
| return { |
| migrated: 0, |
| errors: 0, |
| dryRun: true, |
| summary: { |
| globalViewAccess: categories.globalViewAccess.length, |
| privateGroups: categories.privateGroups.length, |
| total: promptGroupsToMigrate.length, |
| }, |
| details: { |
| globalViewAccess: categories.globalViewAccess.map((g) => ({ |
| name: g.name, |
| _id: g._id, |
| category: g.category || 'uncategorized', |
| permissions: 'Owner + Public VIEW', |
| })), |
| privateGroups: categories.privateGroups.map((g) => ({ |
| name: g.name, |
| _id: g._id, |
| category: g.category || 'uncategorized', |
| permissions: 'Owner only', |
| })), |
| }, |
| }; |
| } |
|
|
| const results = { |
| migrated: 0, |
| errors: 0, |
| publicViewGrants: 0, |
| ownerGrants: 0, |
| }; |
|
|
| |
| for (let i = 0; i < promptGroupsToMigrate.length; i += batchSize) { |
| const batch = promptGroupsToMigrate.slice(i, i + batchSize); |
|
|
| logger.info( |
| `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(promptGroupsToMigrate.length / batchSize)}`, |
| ); |
|
|
| for (const group of batch) { |
| try { |
| const isGlobalGroup = globalPromptGroupIds.has(group._id.toString()); |
|
|
| |
| await grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: group.author, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: group._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| grantedBy: group.author, |
| }); |
| results.ownerGrants++; |
|
|
| |
| if (isGlobalGroup) { |
| await grantPermission({ |
| principalType: PrincipalType.PUBLIC, |
| principalId: null, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: group._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, |
| grantedBy: group.author, |
| }); |
| results.publicViewGrants++; |
| } |
|
|
| results.migrated++; |
| logger.debug( |
| `Migrated promptGroup "${group.name}" [${isGlobalGroup ? 'Global View' : 'Private'}]`, |
| { |
| groupId: group._id, |
| author: group.author, |
| isGlobalGroup, |
| }, |
| ); |
| } catch (error) { |
| results.errors++; |
| logger.error(`Failed to migrate promptGroup "${group.name}"`, { |
| groupId: group._id, |
| author: group.author, |
| error: error.message, |
| }); |
| } |
| } |
|
|
| |
| await new Promise((resolve) => setTimeout(resolve, 100)); |
| } |
|
|
| logger.info('PromptGroup migration completed', results); |
| return results; |
| } |
|
|
| if (require.main === module) { |
| const dryRun = process.argv.includes('--dry-run'); |
| const batchSize = |
| parseInt(process.argv.find((arg) => arg.startsWith('--batch-size='))?.split('=')[1]) || 100; |
|
|
| migrateToPromptGroupPermissions({ dryRun, batchSize }) |
| .then((result) => { |
| if (dryRun) { |
| console.log('\n=== DRY RUN RESULTS ==='); |
| console.log(`Total promptGroups to migrate: ${result.summary.total}`); |
| console.log(`- Global View Access: ${result.summary.globalViewAccess} promptGroups`); |
| console.log(`- Private PromptGroups: ${result.summary.privateGroups} promptGroups`); |
|
|
| if (result.details.globalViewAccess.length > 0) { |
| console.log('\nGlobal View Access promptGroups (first 10):'); |
| result.details.globalViewAccess.slice(0, 10).forEach((group, i) => { |
| console.log(` ${i + 1}. "${group.name}" [${group.category}] (${group._id})`); |
| }); |
| } |
|
|
| if (result.details.privateGroups.length > 0) { |
| console.log('\nPrivate promptGroups (first 10):'); |
| result.details.privateGroups.slice(0, 10).forEach((group, i) => { |
| console.log(` ${i + 1}. "${group.name}" [${group.category}] (${group._id})`); |
| }); |
| } |
|
|
| console.log('\nTo run the actual migration, remove the --dry-run flag'); |
| } else { |
| console.log('\nMigration Results:', JSON.stringify(result, null, 2)); |
| } |
| process.exit(0); |
| }) |
| .catch((error) => { |
| console.error('PromptGroup migration failed:', error); |
| process.exit(1); |
| }); |
| } |
|
|
| module.exports = { migrateToPromptGroupPermissions }; |
|
|