Source: gitHubServer/entities/users.js

/**
 * @fileoverview Users entity for GitHubServer
 * @file users.js
 * @license Apache-2.0
 * @version 3.0.0
 * 
 * @author Michael Hay <michael.hay@mediumroast.io>
 * @copyright 2025 Mediumroast, Inc. All rights reserved.
 */

import { BaseObjects } from '../baseObjects.js';
import { logger } from '../logger.js';

export class Users extends BaseObjects {
  /**
   * @constructor
   * @param {string} token - GitHub API token
   * @param {string} org - GitHub organization name
   * @param {string} processName - Process name for locking
   */
  constructor(token, org, processName) {
    super(token, org, processName, 'Users');
    
    // Add users-specific cache keys
    this._cacheKeys.allUsers = 'all_users';
    this._cacheKeys.authUser = 'auth_user';
    this._cacheKeys.byLogin = `${this.objType}_byLogin`;
    this._cacheKeys.byRole = `${this.objType}_byRole`;
    this._cacheKeys.userActivity = `${this.objType}_activity`;
    this._cacheKeys.orgActivity = 'org_activity';
    
    // Set specific cache timeouts
    this.cacheTimeouts.userDetails = 600000;   // 10 minutes for user details
    this.cacheTimeouts.authUser = 900000;      // 15 minutes for auth user
    this.cacheTimeouts.activity = 300000;      // 5 minutes for activity data
  }

  /**
   * Get all users with enhanced caching
   * @returns {Promise<Array>} List of users
   */
  async getAll() {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'getAll') : 
      { end: () => {} };
    
    try {
      return await this.cache.getOrFetch(
        this._cacheKeys.allUsers,
        async () => this.serverCtl.getAllUsers(),
        this.cacheTimeouts.userDetails || 600000,
        [] // No dependencies
      );
    } catch (error) {
      return this._createError(
        `Failed to retrieve users: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }

  /**
   * Get the authenticated user
   * @returns {Promise<Array>} User information
   */
  async getAuthenticatedUser() {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'getAuthenticatedUser') : 
      { end: () => {} };
    
    try {
      return await this.cache.getOrFetch(
        this._cacheKeys.authUser,
        async () => this.serverCtl.getUser(),
        this.cacheTimeouts.authUser || 900000,
        [] // No dependencies
      );
    } catch (error) {
      return this._createError(
        `Failed to retrieve authenticated user: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Find user by username/login
   * @param {string} login - GitHub username
   * @returns {Promise<Array>} User information
   */
  async findByLogin(login) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'findByLogin') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { login },
        { login: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Use cache with dependency on all users
      const loginCacheKey = `${this._cacheKeys.byLogin}_${login}`;
      
      return await this.cache.getOrFetch(
        loginCacheKey,
        async () => {
          // Get all users
          const allUsersResp = await this.getAll();
          if (!allUsersResp[0]) {
            return allUsersResp;
          }
              
          // Find user with matching login
          const user = allUsersResp[2].find(u => u.login === login);
          if (!user) {
            return this._createError(
              `User with login [${login}] not found`,
              null,
              404
            );
          }
              
          return this._createSuccess(
            `Found user with login [${login}]`,
            user
          );
        },
        this.cacheTimeouts.userDetails || 600000,
        [this._cacheKeys.allUsers] // Depends on all users
      );
    } catch (error) {
      return this._createError(
        `Error finding user: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Find user by role
   * @param {string} role - Role to search for
   * @returns {Promise<Array>} User information
   */
  async findByRole(role) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'findByRole') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { role },
        { role: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Use cache with dependency on all users
      const roleCacheKey = `${this._cacheKeys.byRole}_${role}`;
      
      return await this.cache.getOrFetch(
        roleCacheKey,
        async () => {
          // Get all users
          const allUsersResp = await this.getAll();
          if (!allUsersResp[0]) {
            return allUsersResp;
          }
              
          // Find users with matching role
          const users = allUsersResp[2].filter(u => u.role === role);
          if (users.length === 0) {
            return this._createError(
              `No users found with role [${role}]`,
              null,
              404
            );
          }
              
          return this._createSuccess(
            `Found ${users.length} users with role [${role}]`,
            users
          );
        },
        this.cacheTimeouts.userDetails || 600000,
        [this._cacheKeys.allUsers] // Depends on all users
      );
    } catch (error) {
      return this._createError(
        `Error finding users by role: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Update user role
   * @param {string} login - GitHub username
   * @param {string} newRole - New role to assign
   * @returns {Promise<Array>} Operation result
   */
  async updateUserRole(login, newRole) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'updateUserRole') : 
      { end: () => {} };
    
    try {
      // Validate parameters
      const validationError = this._validateParams(
        { login, newRole },
        { login: 'string', newRole: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Additional validation for role values
      const validRoles = ['admin', 'member', 'billing_manager'];
      if (!validRoles.includes(newRole)) {
        return this._createError(
          `Invalid role: [${newRole}]. Must be one of: ${validRoles.join(', ')}`,
          null,
          400
        );
      }
      
      // Update the user's role
      const result = await this.serverCtl.updateOrgMembership(login, newRole);
          
      if (result[0]) {
        // Invalidate user caches
        this.cache.invalidate(this._cacheKeys.allUsers);
        this.cache.invalidate(`${this._cacheKeys.byLogin}_${login}`);
        
        // Invalidate all role caches as they may have changed
        validRoles.forEach(role => {
          this.cache.invalidate(`${this._cacheKeys.byRole}_${role}`);
        });
      }
          
      return result;
    } catch (error) {
      return this._createError(
        `Failed to update user role: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Get user activity metrics
   * @param {string} login - GitHub username (optional)
   * @returns {Promise<Array>} User activity metrics
   */
  async getUserActivity(login = null) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'getUserActivity') : 
      { end: () => {} };
    
    try {
      // If login provided, get metrics for specific user
      if (login) {
        // Validate parameter
        const validationError = this._validateParams(
          { login },
          { login: 'string' }
        );
            
        if (validationError) return validationError;
        
        // Use cache with specific user activity key
        const userActivityKey = `${this._cacheKeys.userActivity}_${login}`;
        
        return await this.cache.getOrFetch(
          userActivityKey,
          async () => {
            // Find the user first
            const userResp = await this.findByLogin(login);
            if (!userResp[0]) {
              return userResp;
            }
                  
            const activityResp = await this.serverCtl.getUserActivity(login);
            if (!activityResp[0]) {
              return activityResp;
            }
                    
            return this._createSuccess(
              `Retrieved activity for user [${login}]`,
              activityResp[2]
            );
          },
          this.cacheTimeouts.activity || 300000,
          [`${this._cacheKeys.byLogin}_${login}`] // Depends on user data
        );
      } else {
        // Get metrics for all users
        return await this.cache.getOrFetch(
          this._cacheKeys.orgActivity,
          async () => {
            const allUsersResp = await this.getAll();
            if (!allUsersResp[0]) {
              return allUsersResp;
            }
                  
            const orgActivityResp = await this.serverCtl.getOrgActivity();
            if (!orgActivityResp[0]) {
              return orgActivityResp;
            }
                  
            return this._createSuccess(
              'Retrieved organization activity metrics',
              orgActivityResp[2]
            );
          },
          this.cacheTimeouts.activity || 300000,
          [this._cacheKeys.allUsers] // Depends on all users
        );
      }
    } catch (error) {
      return this._createError(
        `Error retrieving activity metrics: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
  
  /**
   * Invite a new user to the organization
   * @param {string} email - User email
   * @param {string} role - Role to assign (admin, member, billing_manager)
   * @returns {Promise<Array>} Operation result
   */
  async inviteUser(email, role = 'member') {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'inviteUser') : 
      { end: () => {} };
    
    try {
      // Validate parameters
      const validationError = this._validateParams(
        { email, role },
        { email: 'string', role: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Additional validation
      if (!email.includes('@')) {
        return this._createError('Invalid email format', null, 400);
      }
      
      // Check role validity
      const validRoles = ['admin', 'member', 'billing_manager'];
      if (!validRoles.includes(role)) {
        return this._createError(
          `Invalid role: [${role}]. Must be one of: ${validRoles.join(', ')}`,
          null,
          400
        );
      }
      
      const result = await this.serverCtl.inviteOrgMember(email, role);
          
      if (result[0]) {
        // Invalidate user caches on success
        this.cache.invalidate(this._cacheKeys.allUsers);
        this.cache.invalidate(`${this._cacheKeys.byRole}_${role}`);
      }
          
      return result;
    } catch (error) {
      return this._createError(
        `Failed to invite user: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
  
  /**
   * Remove a user from the organization
   * @param {string} login - GitHub username
   * @returns {Promise<Array>} Operation result
   */
  async removeUser(login) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'removeUser') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { login },
        { login: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Find user to get their current role before removal
      const userResp = await this.findByLogin(login);
      let userRole = null;
      
      if (userResp[0] && userResp[2] && userResp[2].role) {
        userRole = userResp[2].role;
      }
      
      const result = await this.serverCtl.removeOrgMember(login);
          
      if (result[0]) {
        // Invalidate user caches on success
        this.cache.invalidate(this._cacheKeys.allUsers);
        this.cache.invalidate(`${this._cacheKeys.byLogin}_${login}`);
        
        // Also invalidate role cache if we knew the user's role
        if (userRole) {
          this.cache.invalidate(`${this._cacheKeys.byRole}_${userRole}`);
        } else {
          // If we don't know the role, invalidate all role caches
          this.cache.invalidate(`${this._cacheKeys.byRole}_admin`);
          this.cache.invalidate(`${this._cacheKeys.byRole}_member`);
          this.cache.invalidate(`${this._cacheKeys.byRole}_billing_manager`);
        }
      }
          
      return result;
    } catch (error) {
      return this._createError(
        `Failed to remove user: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
}