Source: gitHubServer/entities/studies.js

/**
 * @fileoverview Studies entity for GitHubServer
 * @file studies.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 Studies 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, 'Studies');
    
    // Add studies-specific cache keys
    this._cacheKeys.byStatus = `${this.objType}_byStatus`;
    this._cacheKeys.byAccess = `${this.objType}_byAccess`; 
    this._cacheKeys.byGroup = `${this.objType}_byGroup`;
    this._cacheKeys.summary = `${this.objType}_summary`;
  }

  /**
   * Delete a study
   * @param {string} objName - Study name to delete
   * @returns {Promise<Array>} Operation result
   */
  async deleteObj(objName) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'deleteObj') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { objName },
        { objName: 'string' }
      );
          
      if (validationError) return validationError;
      
      const source = {
        from: 'Studies',
        to: ['Companies', 'Interactions']
      };
      
      return await this._executeTransaction([
        // Step 1: Catch containers
        async () => {
          let repoMetadata = {
            containers: {
              Studies: {},
              Companies: {},
              Interactions: {}
            }, 
            branch: {}
          };
          return this.serverCtl.catchContainer(repoMetadata);
        },
              
        // Step 2: Get study info
        async (data) => {
          const studyObj = await this.findByX('name', objName, data.containers.Studies.objects);
          if (!studyObj[0]) {
            return studyObj; // Will abort transaction
          }
                  
          // Store linked objects for later steps
          this._tempStudy = studyObj[2][0];
          return this._createSuccess('Found study');
        },
              
        // Step 3: Delete study
        async (data) => {
          const deleteResult = await this.serverCtl.deleteObject(
            objName, 
            source, 
            data, 
            false
          );
                  
          if (!deleteResult[0]) {
            return deleteResult; // Will abort transaction
          }
                  
          return this._createSuccess('Deleted study object');
        },
              
        // Step 4: Release containers
        async (data) => {
          const result = await this.serverCtl.releaseContainer(data);
          if (result[0]) {
            // Invalidate all related caches
            this._invalidateCache();
            
            // Also invalidate related entities' caches
            this.cache.invalidate('container_Companies');
            this.cache.invalidate('container_Interactions');
            if (this.serverCtl.invalidateCache) {
              this.serverCtl.invalidateCache('container_Companies');
              this.serverCtl.invalidateCache('container_Interactions');
            }
          }
          return result;
        }
      ], `delete-study-${objName}`);
    } finally {
      tracking.end();
    }
  }

  /**
   * Find studies by status
   * @param {string} status - Status to search for
   * @returns {Promise<Array>} Search results
   */
  async findByStatus(status) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'findByStatus') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { status },
        { status: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Use cache with dependency on container data
      const statusCacheKey = `${this._cacheKeys.byStatus}_${status}`;
      
      return await this.cache.getOrFetch(
        statusCacheKey,
        () => this.findByX('status', status),
        this.cacheTimeouts[this.objType] || 300000,
        [this._cacheKeys.container] // Depends on all studies
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Find studies by access type (public or private)
   * @param {boolean} isPublic - Whether to find public studies
   * @returns {Promise<Array>} Search results
   */
  async findByAccess(isPublic) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'findByAccess') : 
      { end: () => {} };
    
    try {
      // Validate parameter (as boolean)
      if (typeof isPublic !== 'boolean') {
        return this._createError('isPublic parameter must be a boolean', null, 400);
      }
      
      // Use cache with dependency on container data
      const accessCacheKey = `${this._cacheKeys.byAccess}_${isPublic}`;
      
      return await this.cache.getOrFetch(
        accessCacheKey,
        () => this.findByX('public', isPublic),
        this.cacheTimeouts[this.objType] || 300000,
        [this._cacheKeys.container] // Depends on all studies
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Find studies by group
   * @param {string} group - Group name to search for
   * @returns {Promise<Array>} Search results
   */
  async findByGroup(group) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'findByGroup') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { group },
        { group: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Use cache with dependency on container data
      const groupCacheKey = `${this._cacheKeys.byGroup}_${group}`;
      
      return await this.cache.getOrFetch(
        groupCacheKey,
        async () => {
          // Get all studies
          const allStudiesResp = await this.getAll();
          if (!allStudiesResp[0]) {
            return allStudiesResp;
          }
          
          const allStudies = allStudiesResp[2].mrJson;
          
          // Filter studies by group membership
          const results = allStudies.filter(study => 
            study.groups && study.groups.includes(group)
          );
          
          if (results.length === 0) {
            return this._createError(
              `No studies found in group [${group}]`,
              null,
              404
            );
          }
          
          return this._createSuccess(
            `Found ${results.length} studies in group [${group}]`,
            results
          );
        },
        this.cacheTimeouts[this.objType] || 300000,
        [this._cacheKeys.container] // Depends on all studies
      );
    } catch (error) {
      return this._createError(
        `Error finding studies by group: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Adds an entity to a study
   * @param {string} studyName - Study name
   * @param {string} entityType - Entity type ('Interactions' or 'Companies')
   * @param {string} entityName - Entity name to add
   * @returns {Promise<Array>} Operation result
   */
  async addToStudy(studyName, entityType, entityName) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'addToStudy') : 
      { end: () => {} };
    
    try {
      // Validate parameters
      const validationError = this._validateParams(
        { studyName, entityType, entityName },
        { 
          studyName: 'string', 
          entityType: 'string',
          entityName: 'string'
        }
      );
          
      if (validationError) return validationError;
      
      // Additional validation for entityType
      if (!['Interactions', 'Companies'].includes(entityType)) {
        return this._createError(
          `Invalid entity type: [${entityType}]. Must be 'Interactions' or 'Companies'`,
          null,
          400
        );
      }
      
      return await this._executeTransaction([
        // Step 1: Catch containers
        async () => {
          let repoMetadata = {
            containers: {
              Studies: {},
              [entityType]: {}
            }, 
            branch: {}
          };
          return this.serverCtl.catchContainer(repoMetadata);
        },
              
        // Step 2: Find study and entity
        async (data) => {
          // Find study
          const studyResp = await this.findByX('name', studyName, data.containers.Studies.objects);
          if (!studyResp[0]) {
            return studyResp;
          }
                  
          // Find entity
          const entityClass = new BaseObjects(
            this.serverCtl.token,
            this.serverCtl.orgName,
            'study-manager',
            entityType
          );
                  
          const entityResp = await entityClass.findByX(
            'name', 
            entityName, 
            data.containers[entityType].objects
          );
                  
          if (!entityResp[0]) {
            return this._createError(
              `${entityType} with name [${entityName}] not found`,
              null,
              404
            );
          }
                  
          // Store for next step
          this._tempStudy = studyResp[2][0];
          this._tempEntity = entityResp[2][0];
          return this._createSuccess('Found study and entity');
        },
              
        // Step 3: Update study
        async (data) => {
          // Initialize linked entities field if needed
          const fieldName = `linked_${entityType.toLowerCase()}`;
          if (!this._tempStudy[fieldName]) {
            this._tempStudy[fieldName] = {};
          }
                  
          // Add entity to study
          this._tempStudy[fieldName][entityName] = {
            linked_date: new Date().toISOString()
          };
                  
          // Update study modification date
          this._tempStudy.modification_date = new Date().toISOString();
                  
          // Update the study object in the container
          for (let i = 0; i < data.containers.Studies.objects.length; i++) {
            if (data.containers.Studies.objects[i].name === studyName) {
              data.containers.Studies.objects[i] = this._tempStudy;
              break;
            }
          }
                  
          return this._createSuccess('Updated study with link to entity');
        },
              
        // Step 4: Update entity to reference study
        async (data) => {
          // Add study reference to entity
          const fieldName = 'linked_studies';
          if (!this._tempEntity[fieldName]) {
            this._tempEntity[fieldName] = {};
          }
                  
          // Add study to entity
          this._tempEntity[fieldName][studyName] = {
            linked_date: new Date().toISOString()
          };
                  
          // Update entity modification date
          this._tempEntity.modification_date = new Date().toISOString();
                  
          // Update the entity object in the container
          for (let i = 0; i < data.containers[entityType].objects.length; i++) {
            if (data.containers[entityType].objects[i].name === entityName) {
              data.containers[entityType].objects[i] = this._tempEntity;
              break;
            }
          }
                  
          return this._createSuccess('Updated entity with link to study');
        },
              
        // Step 5: Write study container
        async (data) => {
          const studySha = await this.serverCtl.getSha(
            'Studies', 
            this.objectFiles.Studies, 
            data.branch.name
          );
                  
          if (!studySha[0]) {
            return studySha;
          }
                  
          return await this.serverCtl.writeObject(
            'Studies',
            data.containers.Studies.objects,
            data.branch.name,
            studySha[2]
          );
        },
              
        // Step 6: Write entity container
        async (data) => {
          const entitySha = await this.serverCtl.getSha(
            entityType, 
            this.objectFiles[entityType], 
            data.branch.name
          );
                  
          if (!entitySha[0]) {
            return entitySha;
          }
                  
          return await this.serverCtl.writeObject(
            entityType,
            data.containers[entityType].objects,
            data.branch.name,
            entitySha[2]
          );
        },
              
        // Step 7: Release containers
        async (data) => {
          const result = await this.serverCtl.releaseContainer(data);
          if (result[0]) {
            // Invalidate related caches
            this._invalidateCache();
                      
            // Also invalidate the other entity's cache
            this.cache.invalidate(`container_${entityType}`);
            if (this.serverCtl.invalidateCache) {
              this.serverCtl.invalidateCache(`container_${entityType}`);
            }
          }
          return result;
        }
      ], `add-to-study-${studyName}-${entityName}`);
    } catch (error) {
      return this._createError(
        `Error adding entity to study: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
    
  /**
   * Gets a study summary with statistics
   * @param {string} studyName - Study name 
   * @returns {Promise<Array>} Study summary
   */
  async getStudySummary(studyName) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'getStudySummary') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { studyName },
        { studyName: 'string' }
      );
          
      if (validationError) return validationError;
      
      // Use cache with dependencies on multiple containers
      const summaryCacheKey = `${this._cacheKeys.summary}_${studyName}`;
      
      return await this.cache.getOrFetch(
        summaryCacheKey,
        async () => {
          const studyResp = await this.findByName(studyName);
          if (!studyResp[0]) {
            return studyResp;
          }
          
          const study = studyResp[2][0];
          
          // Prepare summary statistics
          const summary = {
            name: study.name,
            description: study.description,
            status: study.status,
            creation_date: study.creation_date,
            modification_date: study.modification_date,
            statistics: {
              companies: {
                count: study.linked_companies ? Object.keys(study.linked_companies).length : 0,
                items: []
              },
              interactions: {
                count: study.linked_interactions ? Object.keys(study.linked_interactions).length : 0,
                byType: {},
                items: []
              },
              recentActivity: null
            }
          };
          
          // Track most recent activity
          if (study.modification_date) {
            summary.statistics.recentActivity = study.modification_date;
          }
          
          // If there are linked companies, get their details
          if (study.linked_companies && Object.keys(study.linked_companies).length > 0) {
            const companiesClass = new BaseObjects(
              this.serverCtl.token,
              this.serverCtl.orgName,
              'study-summarizer',
              'Companies'
            );
                  
            const allCompaniesResp = await companiesClass.getAll();
            if (allCompaniesResp[0]) {
              const allCompanies = allCompaniesResp[2].mrJson;
                      
              // Find companies linked to this study
              Object.keys(study.linked_companies).forEach(companyName => {
                const company = allCompanies.find(c => c.name === companyName);
                if (company) {
                  summary.statistics.companies.items.push({
                    name: company.name,
                    description: company.description,
                    company_type: company.company_type,
                    linkedDate: study.linked_companies[companyName].linked_date
                  });
                              
                  // Update recent activity if company was modified more recently
                  if (company.modification_date && 
                      (!summary.statistics.recentActivity || 
                       new Date(company.modification_date) > new Date(summary.statistics.recentActivity))) {
                    summary.statistics.recentActivity = company.modification_date;
                  }
                }
              });
            }
          }
          
          // If there are linked interactions, get their details
          if (study.linked_interactions && Object.keys(study.linked_interactions).length > 0) {
            const interactionsClass = new BaseObjects(
              this.serverCtl.token,
              this.serverCtl.orgName,
              'study-summarizer',
              'Interactions'
            );
                  
            const allInteractionsResp = await interactionsClass.getAll();
            if (allInteractionsResp[0]) {
              const allInteractions = allInteractionsResp[2].mrJson;
                      
              // Find interactions linked to this study
              Object.keys(study.linked_interactions).forEach(interactionName => {
                const interaction = allInteractions.find(i => i.name === interactionName);
                if (interaction) {
                  // Track by content type
                  if (interaction.content_type) {
                    summary.statistics.interactions.byType[interaction.content_type] = 
                      (summary.statistics.interactions.byType[interaction.content_type] || 0) + 1;
                  }
                              
                  summary.statistics.interactions.items.push({
                    name: interaction.name,
                    description: interaction.description,
                    content_type: interaction.content_type,
                    file_size: interaction.file_size,
                    linkedDate: study.linked_interactions[interactionName].linked_date
                  });
                              
                  // Update recent activity if interaction was modified more recently
                  if (interaction.modification_date && 
                      (!summary.statistics.recentActivity || 
                       new Date(interaction.modification_date) > new Date(summary.statistics.recentActivity))) {
                    summary.statistics.recentActivity = interaction.modification_date;
                  }
                }
              });
            }
          }
          
          return this._createSuccess(
            `Generated summary for study [${studyName}]`,
            summary
          );
        },
        600000, // Cache for 10 minutes
        [
          this._cacheKeys.container,        // Depends on studies data
          'container_Companies',            // Depends on companies data
          'container_Interactions'          // Depends on interactions data
        ]
      );
    } catch (error) {
      return this._createError(
        `Error generating study summary: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
  
  /**
   * Create a new study
   * @param {Object} studyData - Study data
   * @returns {Promise<Array>} Operation result
   */
  async createStudy(studyData) {
    // Track this operation
    const tracking = logger.trackOperation ? 
      logger.trackOperation(this.objType, 'createStudy') : 
      { end: () => {} };
    
    try {
      // Validate parameter
      const validationError = this._validateParams(
        { studyData },
        { studyData: 'object' }
      );
          
      if (validationError) return validationError;
      
      // Ensure name is present
      if (!studyData.name || typeof studyData.name !== 'string' || !studyData.name.trim()) {
        return this._createError('Study name is required', null, 400);
      }
      
      // Set default values if not provided
      const now = new Date().toISOString();
      const study = {
        name: studyData.name,
        description: studyData.description || '',
        status: studyData.status || 'active',
        public: studyData.public !== undefined ? studyData.public : false,
        groups: Array.isArray(studyData.groups) ? studyData.groups : [],
        creation_date: now,
        modification_date: now
      };
      
      // Create the study using the base createObj method
      return await this.createObj([study]);
    } catch (error) {
      return this._createError(
        `Error creating study: ${error.message}`,
        error,
        500
      );
    } finally {
      tracking.end();
    }
  }
}