Ctrl K
ComunidadGuías…

config/migrate-terms-timestamp.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const path = require('path');
const mongoose = require('mongoose');
const { runAsSystem } = require('@librechat/data-schemas');
const { User } = require('@librechat/data-schemas').createModels(mongoose);
const { countUsers } = require('@librechat/data-schemas').createMethods(mongoose);
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, silentExit } = require('./helpers');
const connect = require('./connect');

/**
 * Migration script for Terms Acceptance Timestamp Tracking
 *
 * This script migrates existing users who have termsAccepted: true but no termsAcceptedAt timestamp.
 * For these users, it sets termsAcceptedAt to their account creation date (createdAt) as a fallback
 * and marks it with termsAcceptedSource: 'migration', so a backfilled date stays distinguishable
 * from one recorded at the moment of acceptance.
 *
 * Usage: npm run migrate:terms-timestamp
 */
(async () => {
  await connect();

  console.purple('--------------------------');
  console.purple('Migrate Terms Acceptance Timestamps');
  console.purple('--------------------------');

  // Count users that need migration. This script spans every tenant, so run
  // it under system context or the tenant isolation plugin throws under
  // TENANT_ISOLATION_STRICT=true and scopes to a non-existent tenant otherwise.
  const usersToMigrate = await runAsSystem(() =>
    countUsers({
      termsAccepted: true,
      $or: [{ termsAcceptedAt: null }, { termsAcceptedAt: { $exists: false } }],
    }),
  );

  if (usersToMigrate === 0) {
    console.green(
      'No users need migration. All users with termsAccepted: true already have a termsAcceptedAt timestamp.',
    );
    silentExit(0);
  }

  console.yellow(
    `Found ${usersToMigrate} user(s) with termsAccepted: true but no termsAcceptedAt timestamp.`,
  );
  console.yellow(
    'These users will have their termsAcceptedAt set to their account creation date (createdAt), marked as backfilled.',
  );

  const confirm = await askQuestion('Are you sure you want to proceed? (y/n): ');

  if (confirm.toLowerCase() !== 'y') {
    console.yellow('Operation cancelled.');
    silentExit(0);
  }

  try {
    // Scan and update across every tenant under system context, matching the
    // other cross-tenant migrations, so the tenant isolation plugin does not
    // throw or scope queries to a non-existent tenant.
    await runAsSystem(async () => {
      const cursor = User.find({
        termsAccepted: true,
        $or: [{ termsAcceptedAt: null }, { termsAcceptedAt: { $exists: false } }],
      }).cursor();

      let migratedCount = 0;
      let skippedCount = 0;
      let errorCount = 0;

      for await (const user of cursor) {
        try {
          // Use createdAt as fallback for termsAcceptedAt
          const termsAcceptedAt = user.createdAt || new Date();
          if (!user.createdAt) {
            console.yellow(
              `Warning: User ${user._id} has no createdAt, using current date for termsAcceptedAt`,
            );
          }
          // Only backfill users who are still accepted and have no timestamp.
          // If they accept through the API or get reset between the cursor read
          // and this write, the filter no longer matches and their state is kept.
          const result = await User.updateOne(
            {
              _id: user._id,
              termsAccepted: true,
              $or: [{ termsAcceptedAt: null }, { termsAcceptedAt: { $exists: false } }],
            },
            { $set: { termsAcceptedAt, termsAcceptedSource: 'migration' } },
          );

          if (result.modifiedCount > 0) {
            migratedCount++;
            if (migratedCount % 100 === 0) {
              console.yellow(`Migrated ${migratedCount} users...`);
            }
          } else {
            skippedCount++;
          }
        } catch (error) {
          console.red(`Error migrating user ${user._id}: ${error.message}`);
          errorCount++;
        }
      }

      console.green(`Migration complete!`);
      console.green(`Successfully migrated: ${migratedCount} user(s)`);
      if (skippedCount > 0) {
        console.yellow(
          `Skipped ${skippedCount} user(s) whose terms state changed during migration.`,
        );
      }
      if (errorCount > 0) {
        console.red(`Errors encountered: ${errorCount}`);
        silentExit(1);
      }
    });
  } catch (error) {
    console.red('Error during migration:', error);
    silentExit(1);
  }

  silentExit(0);
})();

process.on('uncaughtException', (err) => {
  if (!err.message.includes('fetch failed')) {
    console.error('There was an uncaught error:');
    console.error(err);
  }

  if (err.message.includes('fetch failed')) {
    return;
  } else {
    process.exit(1);
  }
});