Source: github.js

/**
 * @fileoverview A class that safely wraps RESTful calls to the GitHub API
 * @license Apache-2.0
 * @version 3.0.0
 * 
 * @author Michael Hay <michael.hay@mediumroast.io>
 * @file github.js
 * @copyright 2025 Mediumroast, Inc. All rights reserved.
 * 
 * @class GitHubFunctions
 * @classdesc Core functions needed to interact with the GitHub API for mediumroast.io.
 * 
 * @requires octokit
 * 
 * @exports GitHubFunctions
 * 
 * @example
 * const gitHubCtl = new GitHubFunctions(accessToken, myOrgName, 'mr-cli-setup')
 * const createRepoResp = await gitHubCtl.createRepository()
 */

import { Octokit } from 'octokit';

// Import refactored modules
import ResponseFactory from './github/response.js';
import ContainerOperations from './github/container.js';
import RepositoryManager from './github/repository.js';
import UserManager from './github/user.js';
import BillingManager from './github/billing.js';
import BranchManager from './github/branch.js';
import { encodeContent, decodeJsonContent, customEncodeURIComponent } from './github/utils.js';
import { isEmpty, isArray, deepClone, mergeObjects, formatDate } from '../utils/helpers.js';

class GitHubFunctions {
  /**
     * @constructor
     * @classdesc Core functions needed to interact with the GitHub API for mediumroast.io.
     * @param {String} token - the GitHub token for the mediumroast.io application
     * @param {String} org - the GitHub organization for the mediumroast.io application
     * @param {String} processName - the name of the process that is using the GitHub API
     * @memberof GitHubFunctions
    */
  constructor(token, org, processName) {
    this.token = token;
    this.orgName = org;
    this.repoName = `${org}_discovery`;
    this.repoDesc = 'A repository for all of the mediumroast.io application assets.';
    this.octCtl = new Octokit({auth: token});
    this.lockFileName = `${processName}.lock`;
    this.mainBranchName = 'main';
    this.objectFiles = {
      Studies: 'Studies.json',
      Companies: 'Companies.json',
      Interactions: 'Interactions.json',
      Users: null,
      Billings: null
    };

    // Initialize our specialized managers
    this.repositoryManager = new RepositoryManager(
      this.octCtl, 
      this.orgName, 
      this.repoName, 
      this.repoDesc,
      this.mainBranchName
    );
        
    this.containerOps = new ContainerOperations(
      this.octCtl,
      this.orgName,
      this.repoName,
      this.mainBranchName,
      this.lockFileName
    );
        
    this.userManager = new UserManager(
      this.octCtl,
      this.orgName, 
      this.repoName
    );
        
    this.billingManager = new BillingManager(
      this.octCtl,
      this.orgName
    );
        
    this.branchManager = new BranchManager(
      this.octCtl,
      this.orgName,
      this.repoName,
      this.mainBranchName
    );

    // Add field map for cross-references as a class property 
    this.fieldMap = {
      Interactions: {
        Companies: 'linked_interactions'
      },
      Companies: {
        Interactions: 'linked_companies'
      },
      Studies: {
        Interactions: 'linked_studies',
        Companies: 'linked_studies'
      }
    };

    // Add a cache for frequently used data
    this._cache = new Map();
    this._defaultTtl = 60000; // 1 minute default TTL

    // Add transaction tracking for complex operations
    this._transactionDepth = 0;
  }

  /**
     * Gets or sets a value in the cache
     * @private
     * @param {String} key - Cache key
     * @param {Function} fetchFn - Function to fetch data if not in cache
     * @param {Number} ttlMs - Time to live in milliseconds
     * @returns {Promise<Array>} Cached or freshly fetched data
     */
  async _getCachedOrFetch(key, fetchFn, ttlMs = this._defaultTtl) {
    const now = Date.now();
    const cached = this._cache.get(key);
        
    // Return cached data if valid
    if (cached && (now - cached.timestamp < ttlMs)) {
      return cached.data;
    }
        
    // Otherwise fetch fresh data
    const result = await fetchFn();
        
    // Only cache successful responses
    if (result[0]) {
      this._cache.set(key, {
        timestamp: now,
        data: result
      });
    }
        
    return result;
  }
    
  /**
     * Invalidate a specific cache entry or the entire cache
     * @param {String} [key] - Optional key to invalidate specific entry
     */
  invalidateCache(key = null) {
    if (key === null) {
      this._cache.clear();
    } else {
      this._cache.delete(key);
    }
  }

  /**
     * Validates parameters against expected types
     * @private
     * @param {Object} params - Parameters to validate
     * @param {Object} expectedTypes - Expected types for each parameter
     * @returns {Array|null} Error response or null if valid
     */
  _validateParams(params, expectedTypes) {
    for (const [name, value] of Object.entries(params)) {
      const expectedType = expectedTypes[name];
      if (!expectedType) continue;
            
      if (expectedType === 'array') {
        if (!isArray(value)) {
          return ResponseFactory.error(
            `Invalid parameter: [${name}] must be an array`,
            null,
            400
          );
        }
      } else if (expectedType === 'object') {
        if (typeof value !== 'object' || value === null) {
          return ResponseFactory.error(
            `Invalid parameter: [${name}] must be an object`,
            null,
            400
          );
        }
      } else if (expectedType === 'string') {
        if (typeof value !== 'string' || isEmpty(value)) {
          return ResponseFactory.error(
            `Invalid parameter: [${name}] must be a non-empty string`,
            null,
            400
          );
        }
      } else if (expectedType === 'boolean') {
        if (typeof value !== 'boolean') {
          return ResponseFactory.error(
            `Invalid parameter: [${name}] must be a boolean`,
            null,
            400
          );
        }
      } else if (expectedType === 'number') {
        if (typeof value !== 'number') {
          return ResponseFactory.error(
            `Invalid parameter: [${name}] must be a number`,
            null,
            400
          );
        }
      }
    }
        
    return null; // No validation errors
  }
    
  /**
     * Executes a series of operations as a transaction
     * @private
     * @param {Array<Function>} operations - Array of async functions to execute
     * @param {String} transactionName - Name of the transaction for logging
     * @returns {Promise<Array>} Result of the transaction
     */
  async _executeTransaction(operations, transactionName) {
    this._transactionDepth++;
    const transactionId = `${transactionName}-${Date.now()}-${this._transactionDepth}`;
        
    try {
      const results = [];
      for (let i = 0; i < operations.length; i++) {
        const operation = operations[i];
        const operationName = operation.name || `Step${i+1}`;
                
        try {
          const result = await operation();
          results.push(result);
                    
          if (!result[0]) {
            // Operation failed, abort transaction
            return ResponseFactory.error(
              `Transaction [${transactionName}] failed at step [${operationName}]: ${result[1]}`,
              { 
                transactionId,
                failedStep: operationName,
                stepResult: result,
                completedSteps: i
              },
              result[3] || 500
            );
          }
        } catch (err) {
          return ResponseFactory.error(
            `Transaction [${transactionName}] failed at step [${operationName}]: ${err.message}`,
            { 
              transactionId,
              failedStep: operationName,
              error: err,
              completedSteps: i
            },
            500
          );
        }
      }
            
      // All operations succeeded
      return ResponseFactory.success(
        `Transaction [${transactionName}] completed successfully`,
        results[results.length - 1][2],
        200
      );
    } finally {
      this._transactionDepth--;
    }
  }

  /**
     * @async
     * @function getSha
     * @description Gets the SHA of a file in a container on a branch
     * @param {String} containerName - the name of the container to get the SHA from
     * @param {String} fileName - the short name of the file to get the SHA from
     * @param {String} branchName - the name of the branch to get the SHA from
     * @returns {Array} An array with position 0 being boolean to signify success/failure, position 1 being the response or error message, and position 2 being the SHA. 
     * @memberof GitHubFunctions
     */
  async getSha(containerName, fileName, branchName) {
    if (isEmpty(containerName) || isEmpty(fileName) || isEmpty(branchName)) {
      return ResponseFactory.error(
        `Missing required parameters: [containerName=${containerName}], [fileName=${fileName}], [branchName=${branchName}]`, 
        null, 
        400
      );
    }
        
    const safePath = `${containerName}/${customEncodeURIComponent(fileName)}`;
    return this.repositoryManager.getSha(safePath, branchName);
  }

  /**
     * @async 
     * @function getUser
     * @description Gets the authenticated user from the GitHub API
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message.
     */
  async getUser() {
    // User data typically stable during a session
    return this._getCachedOrFetch(
      'current_user',
      () => this.userManager.getCurrentUser(),
      300000 // 5 minutes
    );
  }

  /**
     * @async
     * @function getAllUsers
     * @description Gets all of the users from the GitHub API
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message.
     */
  async getAllUsers() {
    // User list changes infrequently - 2 minute cache
    return this._getCachedOrFetch(
      'all_users',
      () => this.userManager.getAllUsers(),
      120000 // 2 minutes
    );
  }

  /**
     * @async
     * @function getActionsBillings
     * @description Gets the complete billing status for actions from the GitHub API
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message.
     */
  async getActionsBillings() {
    return this.billingManager.getActionsBillings();
  }

  /**
     * @async
     * @function getStorageBillings
     * @description Gets the complete billing status for actions from the GitHub API
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message.
     */
  async getStorageBillings() {
    return this.billingManager.getStorageBillings();
  }

  /**
     * @function createRepository
     * @description Creates a repository, at the organization level, for keeping track of all mediumroast.io assets
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the created repo or error message.
     */
  async createRepository() {
    return this.repositoryManager.createRepository();
  }

  /**
     * @function getGitHubOrg
     * @description If the GitHub organization exists retrieves the detail about it and returns to the caller
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the org or error message.
     */
  async getGitHubOrg() {
    // Organization data changes rarely - 5 minute cache
    return this._getCachedOrFetch(
      'org_data',
      () => this.repositoryManager.getOrganization(),
      300000 // 5 minutes
    );
  }

  /**
     * @async
     * @function getWorkflowRuns
     * @description Gets all of the workflow runs for the repository
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the response or error message.
     */
  async getWorkflowRuns() {
    // Workflow runs change more frequently - shorter cache
    return this._getCachedOrFetch(
      'workflow_runs',
      () => this.repositoryManager.getWorkflowRuns(),
      30000 // 30 seconds
    );
  }

  /**
     * @async
     * @function getRepoSize
     * @description Gets the size of the repository in MB
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the response or error message.
     */
  async getRepoSize() {
    // Repo size changes slowly - 1 minute cache
    return this._getCachedOrFetch(
      'repo_size',
      () => this.repositoryManager.getRepoSize(),
      60000 // 1 minute
    );
  }

  /**
     * @function createContainers
     * @description Creates the top level Study, Company and Interaction containers for all mediumroast.io assets
     * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the responses or error messages.
     */
  async createContainers(containers = ['Studies', 'Companies', 'Interactions']) {
    if (!isArray(containers)) {
      return ResponseFactory.error(
        'Invalid parameter: [containers] must be an array', 
        null, 
        400
      );
    }
        
    return this.repositoryManager.createContainers(containers);
  }

  /**
     * @description Creates a new branch from the main branch.
     * @function createBranchFromMain
     * @async
     * @returns {Promise<Array>} A promise that resolves to an array containing a boolean indicating success, a message, and the response.
     */
  async createBranchFromMain() {
    return this.branchManager.createBranchFromMain();
  }

  /**
     * @description Merges a specified branch into the main branch by creating a pull request.
     * @function mergeBranchToMain
     * @async
     * @param {string} branchName - The name of the branch to merge into main.
     * @param {string} mySha - The SHA of the commit to use as the head of the pull request.
     * @param {string} [commitDescription='Performed CRUD operation on objects.'] - The description of the commit.
     * @returns {Promise<Array>} A promise that resolves to an array containing success status, message, and response.
     */
  async mergeBranchToMain(branchName, mySha, commitDescription='Performed CRUD operation on objects.') {
    if (isEmpty(branchName)) {
      return ResponseFactory.error(
        'Missing required parameter: [branchName]', 
        null, 
        400
      );
    }
        
    return this.branchManager.mergeBranchToMain(branchName, mySha, commitDescription);
  }

  /**
     * @description Checks to see if a container is locked.
     * @function checkForLock
     * @async
     * @param {string} containerName - The name of the container to check for a lock.
     * @returns {Promise<Array>} A promise that resolves to an array containing status and message.
     */
  async checkForLock(containerName) {
    if (isEmpty(containerName)) {
      return ResponseFactory.error(
        'Missing required parameter: [containerName]', 
        null, 
        400
      );
    }
        
    return this.containerOps.checkForLock(containerName);
  }

  /**
     * @description Locks a container by creating a lock file in the container.
     * @function lockContainer
     * @async
     * @param {string} containerName - The name of the container to lock.
     * @returns {Promise<Array>} A promise that resolves to an array containing status and message.
     */
  async lockContainer(containerName) {
    if (isEmpty(containerName)) {
      return ResponseFactory.error(
        'Missing required parameter: [containerName]', 
        null, 
        400
      );
    }
        
    return this.containerOps.lockContainer(containerName);
  }

  /**
     * @description Unlocks a container by deleting the lock file in the container.
     * @function unlockContainer
     * @async
     * @param {string} containerName - The name of the container to unlock.
     * @param {string} commitSha - The SHA of the commit to use as the head of the pull request.
     * @param {string} branchName - The name of the branch to unlock the container on.
     * @returns {Promise<Array>} A promise that resolves to an array containing status and message.
     */
  async unlockContainer(containerName, commitSha, branchName = this.mainBranchName) {
    if (isEmpty(containerName) || isEmpty(commitSha)) {
      return ResponseFactory.error(
        `Missing required parameters: [containerName=${containerName}], [commitSha=${commitSha}]`, 
        null, 
        400
      );
    }
        
    return this.containerOps.unlockContainer(containerName, commitSha, branchName);
  }

  /**
     * Read a blob (file) from a container (directory) in a specific branch.
     * @param {string} fileName - The name of the blob to read with a complete path to the file.
     * @returns {Array} A list containing success status, message, and the blob's raw data.
     */
  async readBlob(fileName) {
    if (isEmpty(fileName)) {
      return ResponseFactory.error(
        'Missing required parameter: [fileName]', 
        null, 
        400
      );
    }
        
    // Create an enhanced repository manager method that handles decoding
    return this.repositoryManager.readBlobWithDecoding(
      customEncodeURIComponent(fileName), 
      this.token
    );
  }

  /**
     * Delete a blob (file) from a container (directory)
     * @param {string} containerName - The container name
     * @param {string} fileName - The file name
     * @param {string} branchName - The branch name
     * @param {string} sha - The SHA of the file
     * @returns {Array} A list containing success status, message, and response
     */
  async deleteBlob(containerName, fileName, branchName, sha) {
    if (isEmpty(containerName) || isEmpty(fileName) || isEmpty(branchName) || isEmpty(sha)) {
      return ResponseFactory.error(
        `Missing required parameters: [containerName=${containerName}], [fileName=${fileName}], [branchName=${branchName}], [sha=${sha}]`, 
        null, 
        400
      );
    }
        
    const safePath = `${containerName}/${customEncodeURIComponent(fileName)}`;
    return this.repositoryManager.deleteBlob(safePath, branchName, sha);
  }

  /**
     * Write a blob (file) to a container (directory)
     * @param {string} containerName - The container name
     * @param {string} fileName - The file name
     * @param {string} blob - The blob to write
     * @param {string} branchName - The branch name
     * @param {string} sha - The SHA of the file if updating
     * @returns {Array} A list containing success status, message, and response
     */
  async writeBlob(containerName, fileName, blob, branchName, sha) {
    if (isEmpty(containerName) || isEmpty(fileName) || isEmpty(branchName)) {
      return ResponseFactory.error(
        `Missing required parameters: [containerName=${containerName}], [fileName=${fileName}], [branchName=${branchName}]`, 
        null, 
        400
      );
    }
        
    const encodedContent = typeof blob === 'string' ? encodeContent(blob) : blob;
        
    return this.repositoryManager.writeBlob(
      containerName, 
      customEncodeURIComponent(fileName), 
      encodedContent, 
      branchName, 
      sha
    );
  }

  /**
     * @function writeObject
     * @description Writes an object to a specified container using the GitHub API.
     * @async
     * @param {string} containerName - The name of the container to write the object to.
     * @param {object} obj - The object to write to the container.
     * @param {string} ref - The reference to use when writing the object.
     * @param {string} mySha - The SHA of the current file if updating.
     * @returns {Promise<Array>} Status, message, and response
     */
  async writeObject(containerName, obj, ref, mySha) {
    // Invalidate relevant caches on write
    this.invalidateCache('repo_size');
    this.invalidateCache(`container_${containerName}`);
        
    if (isEmpty(containerName) || obj === null || isEmpty(ref)) {
      return ResponseFactory.error(
        `Missing required parameters: [containerName=${containerName}], [obj=${obj !== null ? 'present' : 'null'}], [ref=${ref}]`, 
        null, 
        400
      );
    }
        
    const content = encodeContent(obj);
        
    return this.repositoryManager.writeBlob(
      containerName,
      this.objectFiles[containerName],
      content,
      ref,
      mySha
    );
  }

  /**
     * @function readObjects
     * @description Reads objects from a specified container using the GitHub API.
     * @async
     * @param {string} containerName - The name of the container to read objects from.
     * @returns {Promise<Array>} Status, message, and contents
     */
  async readObjects(containerName) {
    if (isEmpty(containerName) || isEmpty(this.objectFiles[containerName])) {
      return ResponseFactory.error(
        `Invalid container name or no object file defined for ${containerName}`, 
        null, 
        400
      );
    }
        
    const path = `${containerName}/${this.objectFiles[containerName]}`;
    const result = await this.repositoryManager.getContent(
      path, 
      this.mainBranchName
    );

    if (!result[0]) {
      return result;
    }

    const jsonContent = decodeJsonContent(result[2].content);
        
    if (jsonContent === null) {
      return ResponseFactory.error(`Unable to parse [${path}] as JSON`, new Error('JSON parse error'));
    }
        
    result[2].mrJson = jsonContent;
    return result;
  }

  /**
     * @function updateObject
     * @description Updates an object in a specified container
     * @async
     * @param {string} containerName - The name of the container containing the object
     * @param {string} objName - The name of the object to update
     * @param {string} key - The key of the object to update
     * @param {string} value - The value to update the key with
     * @param {boolean} [dontWrite=false] - A flag to indicate if the object should be written back
     * @param {boolean} [system=false] - A flag to indicate if the update is a system call
     * @param {Array} [whiteList=[]] - A list of keys that are allowed to be updated
     * @returns {Promise<Array>} Status, message, and response
     */
  async updateObject(containerName, objName, key, value, dontWrite=false, system=false, whiteList=[]) {
    // Validate parameters using the new validation method
    const validationError = this._validateParams(
      { containerName, objName, key, dontWrite, system, whiteList },
      { 
        containerName: 'string', 
        objName: 'string', 
        key: 'string',
        dontWrite: 'boolean',
        system: 'boolean',
        whiteList: 'array'
      }
    );
        
    if (validationError) return validationError;
        
    // Authorization check
    if (!system && !whiteList.includes(key)) {
      return ResponseFactory.error(
        `Unauthorized operation: Updating the key [${key}] is not supported`,
        null,
        403
      );
    }

    // Use transaction pattern for the complex update operation
    return this._executeTransaction([
      // Step 1: Read the objects
      async () => {
        const readResponse = await this.readObjects(containerName);
        if (!readResponse[0]) {
          return ResponseFactory.error(
            `Unable to read source objects from [${containerName}]`,
            readResponse[2],
            500
          );
        }
                
        // Store the read response for next steps
        this._tempReadResponse = readResponse;
        return readResponse;
      },
            
      // Step 2: Catch the container if needed
      async () => {
        if (dontWrite) {
          return ResponseFactory.success(
            'Skipping container locking for read-only update',
            null
          );
        }
                
        const repoMetadata = {containers: {}, branch: {}};
        repoMetadata.containers[containerName] = {};
        const caught = await this.catchContainer(repoMetadata);
                
        // Store the caught data for next steps
        this._tempCaught = caught;
        return caught;
      },
            
      // Step 3: Update the object
      async () => {
        const objectsCopy = deepClone(this._tempReadResponse[2].mrJson);
        let objectFound = false;
                
        for (const obj in objectsCopy) {
          if (objectsCopy[obj].name === objName) {
            objectFound = true;
            const updates = { 
              [key]: value,
              modification_date: formatDate(new Date())
            };
            objectsCopy[obj] = mergeObjects(objectsCopy[obj], updates);
          }
        }
                
        if (!objectFound) {
          return ResponseFactory.error(
            `Object with name [${objName}] not found in [${containerName}]`,
            null,
            404
          );
        }
                
        // Store updated objects for next steps
        this._tempUpdatedObjects = objectsCopy;
                
        if (dontWrite) {
          return ResponseFactory.success(
            `Merged updates object(s) with [${containerName}] objects`,
            objectsCopy
          );
        }
                
        return ResponseFactory.success('Object updated in memory', objectsCopy);
      },
            
      // Step 4: Write the objects (if not dontWrite)
      async () => {
        if (dontWrite) {
          return ResponseFactory.success(
            'Skipping writing for read-only update',
            this._tempUpdatedObjects
          );
        }
                
        const writeResponse = await this.writeObject(
          containerName, 
          this._tempUpdatedObjects, 
          this._tempCaught[2].branch.name,
          this._tempCaught[2].containers[containerName].objectSha
        );
                
        return writeResponse;
      },
            
      // Step 5: Release the container (if not dontWrite)
      async () => {
        if (dontWrite) {
          return ResponseFactory.success(
            'Skipping container release for read-only update',
            this._tempUpdatedObjects
          );
        }
                
        return this.releaseContainer(this._tempCaught[2]);
      }
    ], `update-object-${containerName}-${objName}`);
  }

  /**
     * @function deleteObject
     * @description Deletes an object from a specified container
     * @async
     * @param {string} objName - The name of the object to delete
     * @param {object} source - The source object that contains the from and to containers
     * @param {object} repoMetadata - The repository metadata
     * @param {boolean} catchIt - Whether to catch the container
     * @returns {Promise<Array>} Status, message, and response
     */
  async deleteObject(objName, source, repoMetadata=null, catchIt=true) {
    // Validate parameters
    const validationError = this._validateParams(
      { objName, source, catchIt },
      { 
        objName: 'string',
        source: 'object',
        catchIt: 'boolean'
      }
    );
        
    if (validationError) return validationError;
        
    // Additional validation for source object
    if (isEmpty(source.from) || !isArray(source.to) || source.to.length === 0) {
      return ResponseFactory.error(
        'Invalid source configuration: [from] must be a non-empty string and [to] must be a non-empty array',
        null,
        400
      );
    }
        
    // Define transaction steps
    const deleteSteps = [];
        
    // Step 1: Catch container if needed
    if (catchIt) {
      deleteSteps.push(async () => {
        const metadata = {containers: {}, branch: {}};
        metadata.containers[source.from] = {};
        metadata.containers[source.to[0]] = {};
        const caught = await this.catchContainer(metadata);
                
        // Store for later steps
        this._tempRepoMetadata = deepClone(caught[2]);
        return caught;
      });
    } else {
      deleteSteps.push(async () => {
        this._tempRepoMetadata = repoMetadata;
        return ResponseFactory.success('Using provided repository metadata', repoMetadata);
      });
    }
        
    // Step 2: Find and delete the object
    deleteSteps.push(async () => {
      let objectFound = false;
      for (const obj in this._tempRepoMetadata.containers[source.from].objects) {
        if (this._tempRepoMetadata.containers[source.from].objects[obj].name === objName) {
          objectFound = true;
                    
          // For Interactions, delete the actual file
          if (source.from === 'Interactions') {
            const fileName = this._tempRepoMetadata.containers[source.from].objects[obj].url;
                        
            try {
              const { data } = await this.octCtl.rest.repos.getContent({
                owner: this.orgName,
                repo: this.repoName,
                path: customEncodeURIComponent(fileName)
              });
                            
              const fileBits = fileName.split('/');
              const shortFilename = fileBits[fileBits.length - 1];
                            
              const deleteResponse = await this.deleteBlob(
                source.from, 
                shortFilename, 
                this._tempRepoMetadata.branch.name,
                data.sha
              );
                            
              if (!deleteResponse[0]) {
                return deleteResponse; // Transaction will abort
              }
            } catch (err) {
              return ResponseFactory.error(
                `Failed to get content for [${fileName}]: ${err.message}`,
                err,
                503
              );
            }
          }
                    
          // Remove the object from the array
          this._tempRepoMetadata.containers[source.from].objects.splice(obj, 1);
          break;
        }
      }
            
      if (!objectFound) {
        return ResponseFactory.error(
          `Object with name [${objName}] not found in [${source.from}]`,
          null,
          404
        );
      }
            
      return ResponseFactory.success(`Found and removed object [${objName}]`, null);
    });
        
    // Step 3: Update references in linked objects
    deleteSteps.push(async () => {
      for (const obj in this._tempRepoMetadata.containers[source.to[0]].objects) {
        if (this._tempRepoMetadata.containers[source.to[0]].objects[obj][this.fieldMap[source.from][source.to[0]]] && 
                    objName in this._tempRepoMetadata.containers[source.to[0]].objects[obj][this.fieldMap[source.from][source.to[0]]]) {
                    
          delete this._tempRepoMetadata.containers[source.to[0]].objects[obj][this.fieldMap[source.from][source.to[0]]][objName];
                    
          // Update modification date using the helper function
          this._tempRepoMetadata.containers[source.to[0]].objects[obj].modification_date = formatDate(new Date());
        }
      }
            
      return ResponseFactory.success('Updated cross-references', null);
    });
        
    // Add steps for writing changes
    deleteSteps.push(
      // Step 4: Write source container
      async () => {
        const fromSha = await this.getSha(source.from, this.objectFiles[source.from], this._tempRepoMetadata.branch.name);
        if (!fromSha[0]) {
          return fromSha; // Transaction will abort
        }
                
        return this.writeObject(
          source.from, 
          this._tempRepoMetadata.containers[source.from].objects, 
          this._tempRepoMetadata.branch.name,
          fromSha[2]
        );
      },
            
      // Step 5: Write target container
      async () => {
        const toSha = await this.getSha(source.to[0], this.objectFiles[source.to[0]], this._tempRepoMetadata.branch.name);
        if (!toSha[0]) {
          return toSha; // Transaction will abort
        }
                
        return this.writeObject(
          source.to[0], 
          this._tempRepoMetadata.containers[source.to[0]].objects, 
          this._tempRepoMetadata.branch.name,
          toSha[2]
        );
      }
    );
        
    // Step 6: Release container if needed
    if (catchIt) {
      deleteSteps.push(async () => {
        return this.releaseContainer(this._tempRepoMetadata);
      });
    } else {
      deleteSteps.push(async () => {
        return ResponseFactory.success(
          `Deleted [${source.from}] object of the name [${objName}] without releasing container`,
          null
        );
      });
    }
        
    // Execute the delete transaction
    return this._executeTransaction(deleteSteps, `delete-object-${source.from}-${objName}`);
  }

  /**
     * @function catchContainer
     * @description Catches a container by locking it, creating a new branch, reading the objects
     * @param {Object} repoMetadata - The metadata object
     * @returns {Promise<Array>} Status, message, and metadata
     */
  async catchContainer(repoMetadata) {
    if (!repoMetadata || !repoMetadata.containers || Object.keys(repoMetadata.containers).length === 0) {
      return ResponseFactory.error(
        'Invalid parameter: [repoMetadata] must contain containers property with at least one container', 
        null, 
        400
      );
    }
        
    return this.containerOps.catchContainers(
      repoMetadata,
      this.objectFiles,
      () => this.branchManager.createBranchFromMain(),
      (container) => this.readObjects(container)
    );
  }

  /**
     * @function releaseContainer
     * @description Releases a container by unlocking it and merging the branch
     * @param {Object} repoMetadata - The metadata object
     * @returns {Promise<Array>} Status, message, and response
     */
  async releaseContainer(repoMetadata) {
    if (!repoMetadata || !repoMetadata.containers || !repoMetadata.branch) {
      return ResponseFactory.error(
        'Invalid parameter: [repoMetadata] must contain containers and branch information', 
        null, 
        400
      );
    }
        
    return this.containerOps.releaseContainers(
      repoMetadata,
      (branchName, branchSha) => this.branchManager.mergeBranchToMain(branchName, branchSha)
    );
  }
}

export default GitHubFunctions;