/**
* @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;