mediumroast_js
Version:
A Command Line Interface (CLI) and Javascript SDK to interact with Mediumroast for GitHub.
1,144 lines (1,037 loc) • 50.3 kB
JavaScript
/**
* @fileoverview A class that safely wraps RESTful calls to the GitHub API
* @license Apache-2.0
* @version 1.0.0
*
* @author Michael Hay <michael.hay@mediumroast.io>
* @file github.js
* @copyright 2024 Mediumroast, Inc. All rights reserved.
*
* @class GitHubFunctions
* @classdesc Core functions needed to interact with the GitHub API for mediumroast.io.
*
* @requires octokit
* @requires axios
*
* @exports GitHubFunctions
*
* @example
* const gitHubCtl = new GitHubFunctions(accessToken, myOrgName, 'mr-cli-setup')
* const createRepoResp = await gitHubCtl.createRepository()
*/
import { Octokit } from "octokit"
import axios from "axios"
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})
// NOTE: The lockfile name needs to be more flexible in checking for the lockfile
this.lockFileName = `${processName}.lock`
this.mainBranchName = 'main'
this.objectFiles = {
Studies: 'Studies.json',
Companies: 'Companies.json',
Interactions: 'Interactions.json',
Users: null,
Billings: null
}
}
/**
* @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) {
try {
const response = await this.octCtl.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
ref: branchName,
path: `${containerName}/${fileName}`
})
return [true, {status_code:200, status_msg: `captured sha for [${containerName}/${fileName}]`}, response.data.sha]
} catch (err) {
return [false, {status_code: 500, status_msg: `unable to capture sha for [${containerName}/${fileName}] due to [${err.message}]`}, err]
}
}
/**
* @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.
* @todo Add a check to see if the user is a member of the organization
* @todo Add a check to see if the user has admin rights to the organization
*/
async getUser() {
// using try and catch to handle errors get user info
try {
const response = await this.octCtl.rest.users.getAuthenticated()
return [true, `SUCCESS: able to capture current user info`, response.data]
} catch (err) {
return [false, `ERROR: unable to capture current user info due to [${err}]`, err.message]
}
}
/**
* @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() {
// using try and catch to handle errors get info for all users
try {
const response = await this.octCtl.rest.repos.listCollaborators({
owner: this.orgName,
repo: this.repoName,
affiliation: 'all'
})
return [true, `SUCCESS: able to capture info for all users`, response.data]
} catch (err) {
return [false, `ERROR: unable to capture info for all users due to [${err}]`, err.message]
}
}
/**
* @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() {
// using try and catch to handle errors get info for all billings data
try {
const response = await this.octCtl.rest.billing.getGithubActionsBillingOrg({
org: this.orgName,
})
return [true, `SUCCESS: able to capture info for actions billing`, response.data]
} catch (err) {
return [false, {status_code: 404, status_msg: `unable to capture info for actions billing due to [${err}]`}, err.message]
}
}
/**
* @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() {
// using try and catch to handle errors get info for all billings data
try {
const response = await this.octCtl.rest.billing.getSharedStorageBillingOrg({
org: this.orgName,
})
return [true, `SUCCESS: able to capture info for storage billing`, response.data]
} catch (err) {
return [false, {status_code: 404, status_msg: `unable to capture info for storage billing due to [${err}]`}, err.message]
}
}
/**
* @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.
* @todo Make sure the repo is not public
*/
async createRepository () {
try {
const response = await this.octCtl.rest.repos.createInOrg({
org: this.orgName,
name: this.repoName,
description: this.repoDesc,
private: true
})
return [true, response.data]
} catch (err) {
return[false, err.message]
}
}
/**
* @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 () {
try {
const response = await this.octCtl.rest.orgs.get({
org: this.orgName
})
return[true, response.data]
} catch (err) {
return[false, err.message]
}
}
/**
* @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 () {
let workflows
try {
workflows = await this.octCtl.rest.actions.listWorkflowRunsForRepo({
owner: this.orgName,
repo: this.repoName
})
} catch (err) {
return [false, {status_code: 500, status_msg: err.message}, err]
}
const workflowList = []
let totalRunTimeThisMonth = 0
for (const workflow of workflows.data.workflow_runs) {
// Get the current month
const currentMonth = new Date().getMonth()
// Compute the runtime and if the time is less than 60s round it to 1m
const runTime = Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60) < 1 ? 1 : Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60)
// If the month of the workflow is not the current month, then skip it
if (new Date(workflow.updated_at).getMonth() !== currentMonth) {
continue
}
totalRunTimeThisMonth += runTime
// Add the workflow to the workflowList
workflowList.push({
// Create name where the path is the name of the workflow, but remove the path and the .yml extension
name: workflow.path.replace('.github/workflows/', '').replace('.yml', ''),
title: workflow.display_title,
id: workflow.id,
workflowId: workflow.workflow_id,
runTimeMinutes: runTime,
status: workflow.status,
conclusion: workflow.conclusion,
event: workflow.event,
path: workflow.path,
})
}
// Sort the worflowList to put the most recent workflows first
workflowList.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
return [true, {
status_code: 200,
status_msg: `discovered [${workflowList.length}] workflow runs for [${this.repoName}]`,},
workflowList
]
}
// Create a method using the octokit to get the size of the repository and return the size in MB
async getRepoSize() {
let repoData = {
size: 0,
numFiles: 0,
name: this.repoName,
org: this.orgName,
}
// Count the number of files in the repository
const countFiles = async (path = '') => {
try {
const response = await this.octCtl.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
path: path
})
let fileCount = 0;
for (const item of response.data) {
if (item.type === 'file') {
fileCount += 1;
} else if (item.type === 'dir') {
fileCount += await countFiles(item.path)
}
}
return fileCount
} catch (err) {
return 0
}
}
try {
repoData.numFiles = await countFiles()
} catch (err) {
repoData.numFiles = 'Unknown'
}
const getRepoSize = async () => {
try {
const response = await this.octCtl.rest.repos.get({
owner: this.orgName,
repo: this.repoName
})
const sizeInKB = response.data.size;
const sizeInMB = sizeInKB / 1024;
return sizeInMB.toFixed(2); // Convert to MB and format to 2 decimal places
} catch (err) {
return 0
}
}
try {
repoData.size = await getRepoSize()
} catch (err) {
repoData.size = 'Unknown'
}
return [true, {status_code: 200, status_msg: `discovered size of [${this.repoName}]`}, repoData]
}
/**
* @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']) {
let responses = []
let emptyJson = Buffer.from(JSON.stringify([])).toString('base64')
for (const containerName in containers) {
try {
const response = await this.octCtl.rest.repos.createOrUpdateFileContents({
owner: this.orgName,
repo: this.repoName,
path: `${containers[containerName]}/${containers[containerName]}.json`,
message: `Create container [${containers[containerName]}]`,
content: emptyJson, // Create a valid empty JSON file, but this must be Base64 encoded
})
responses.push(response)
} catch (err) {
return[false, err]
}
}
return [true, responses]
}
/**
* @description Creates a new branch from the main branch.
* @function createBranchFromMain
* @async
* @returns {Promise<Array<boolean, string, object>>} A promise that resolves to an array containing a boolean indicating success, a success message, and the response from the GitHub API.
* @throws {Error} If an error occurs while getting the main branch reference or creating the new branch.
* @memberof GitHubFunctions
*/
async createBranchFromMain() {
// Define the branch name
const branchName = Date.now().toString()
try {
// Get the SHA of the latest commit on the main branch
const mainBranchRef = await this.octCtl.rest.git.getRef({
owner: this.orgName,
repo: this.repoName,
ref: `heads/${this.mainBranchName}`,
})
// Create a new branch based on the latest commit on the main branch
const newBranchResp = await this.octCtl.rest.git.createRef({
owner: this.orgName,
repo: this.repoName,
ref: `refs/heads/${branchName}`,
sha: mainBranchRef.data.object.sha,
})
return [true, `SUCCESS: created branch [${branchName}]`, newBranchResp]
} catch (error) {
return [false, `FAILED: unable to create branch [${branchName}] due to [${error.message}]`, newBranchResp]
}
}
/**
* @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<Aray<boolean, string, object>>} A promise that resolves to an array containing a boolean indicating success, a success message, and the response from the GitHub API.
* @throws {Error} If an error occurs while creating the branch or the pull request.
* @memberof GitHubFunctions
*/
async mergeBranchToMain(branchName, mySha, commitDescription='Performed CRUD operation on objects.') {
try {
// Create a new branch
// const createBranchResponse = await this.octCtl.rest.git.createRef({
// owner: this.orgName,
// repo: this.repoName,
// ref: branchName,
// sha: mySha,
// })
// console.log(createBranchResponse.data)
// Create a pull request
const createPullRequestResponse = await this.octCtl.rest.pulls.create({
owner: this.orgName,
repo: this.repoName,
title: commitDescription,
head: branchName,
base: this.mainBranchName,
body: commitDescription,
})
// Merge the pull request
const mergeResponse = await this.octCtl.rest.pulls.merge({
owner: this.orgName,
repo: this.repoName,
pull_number: createPullRequestResponse.data.number,
commit_title: commitDescription,
})
return [true, 'SUCCESS: Pull request created and merged successfully', mergeResponse]
} catch (error) {
return [false, `FAILED: Pull request not created or merged successfully due to [${error.message}]`, null]
}
}
/**
* @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<boolean, string>>} A promise that resolves to an array containing a boolean indicating success and a message.
* @throws {Error} If an error occurs while getting the latest commit or the contents of the container.
* @memberof GitHubFunctions
* @todo Add a check to see if the lock file is older than 24 hours and if so delete it.
*/
async checkForLock(containerName) {
// Get the latest commit
const latestCommit = await this.octCtl.rest.repos.getCommit({
owner: this.orgName,
repo: this.repoName,
ref: this.mainBranchName,
})
// Check to see if the lock file exists
const mainContents = await this.octCtl.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
ref: latestCommit.data.sha,
path: containerName
})
// Can we search for a file with an extension of .lock?
// This is due to the fact that there are other processes that may create lock files.
const lockExists = mainContents.data.some(
item => item.path === `${containerName}/${this.lockFileName}`
)
if (lockExists) {
return [true, {status_code: 200, status_msg: `container [${containerName}] is locked with lock file [${this.lockFileName}]`}, lockExists]
} else {
return [false, {status_code: 404, status_msg: `container [${containerName}] is not locked with lock file [${this.lockFileName}]`}, lockExists]
}
}
/**
* @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<boolean, string, object>>} A promise that resolves to an array containing a boolean indicating success, a success message, and the response from the GitHub API.
* @throws {Error} If an error occurs while getting the latest commit or creating the lock file.
* @memberof GitHubFunctions
*/
async lockContainer(containerName) {
// Define the full path to the lockfile
const lockFile = `${containerName}/${this.lockFileName}`
// Get the latest commit
const {data: latestCommit} = await this.octCtl.rest.repos.getCommit({
owner: this.orgName,
repo: this.repoName,
ref: this.mainBranchName,
})
let lockResponse
try {
lockResponse = await this.octCtl.rest.repos.createOrUpdateFileContents({
owner: this.orgName,
repo: this.repoName,
path: lockFile,
content: '',
branch: this.mainBranchName,
message: `Locking container [${containerName}]`,
sha: latestCommit.sha
})
return [true, `SUCCESS: Locked the container [${containerName}]`, lockResponse]
} catch(err) {
return [false, `FAILED: Unable to lock the container [${containerName}]`, err]
}
}
/**
* @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.
* @returns {Promise<Array<boolean, string, object>>} A promise that resolves to an array containing a boolean indicating success, a success message, and the response from the GitHub API.
* @throws {Error} If an error occurs while getting the latest commit or deleting the lock file.
* @memberof GitHubFunctions
*/
async unlockContainer(containerName, commitSha, branchName = this.mainBranchName) {
// Define the full path to the lockfile
const lockFile = `${containerName}/${this.lockFileName}`
const lockExists = await this.checkForLock(containerName)
// TODO: Change to try and catch
if(lockExists[0]) {
// NOTICE: DON'T USE DELETE AS THIS COMPLETELY REMOVES THE REPOSITORY WITHOUT MUCH WARNING
const unlockResponse = await this.octCtl.rest.repos.deleteFile({
owner: this.orgName,
repo: this.repoName,
path: lockFile,
branch: branchName,
message: `Unlocking container [${containerName}]`,
sha: commitSha
})
return [true, `SUCCESS: Unlocked the container [${containerName}]`, unlockResponse]
} else {
return [false, `FAILED: Unable to unlock the container [${containerName}]`, null]
}
}
/**
* 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 (e.g. dirname/filename.ext).
* @returns {Array} A list containing a boolean indicating success or failure, a status message, and the blob's raw data (or the error message in case of failure).
*/
async readBlob(fileName) {
// Encode the file name including files with special characters like question marks
// Custom encoding function to handle special characters
const customEncodeURIComponent = (str) => {
return str.split('').map(char => {
return encodeURIComponent(char).replace(/[!'()*]/g, (c) => {
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
});
}).join('');
}
const originalFileNameEncoded = customEncodeURIComponent(fileName)
// Try to download the file from the repository using the download URL
const downloadFile = async (url) => {
try {
const downloadResult = await axios.get(url, { responseType: 'arraybuffer' })
return [true, downloadResult.data]
} catch (e) {
if (e instanceof TypeError && (e.message.includes('Request path contains unescaped characters') || e.message.includes('ERR_UNESCAPED_CHARACTERS'))) {
// Handle the specific error here
// For example, you can re-encode the URL or log the error
return [false, 'ERR_UNESCAPED_CHARACTERS']
}
return [false, e]
}
}
// Re-encode the download URL
const reEncodeDownloadUrl = (url, originalFileName) => {
// Extract the base URL and the file name part
let urlParts = url.split('/');
const lastPart = urlParts.pop(); // Get the last part of the URL which contains the file name and possibly query parameters
// Remove the last item from the URL parts
urlParts.pop()
// Find the position of the first question mark that indicates the start of query parameters
const altLastPart = lastPart.split('?')
const queryParams = altLastPart[altLastPart.length - 1]
// Encode the file name part using encodeURIComponent
// const encodedFileNamePart = encodeURIComponent(fileNamePart);
// Reconstruct the download URL
return `${urlParts.join('/')}/${originalFileName}${queryParams ? '?' + queryParams : ''}`;
}
// Encode the file name and obtain the download URL
const encodedFileName = encodeURIComponent(fileName)
// Set the object URL
const objectUrl = `https://api.github.com/repos/${this.orgName}/${this.repoName}/contents/${encodedFileName}`
// Set the headers
const headers = { 'Authorization': `token ${this.token}` }
// Obtain the download URL
const result = await axios.get(objectUrl, { headers })
let downloadUrl = result.data.download_url
// Attempt to download the file from the repository
let blobData = await downloadFile(downloadUrl)
// Check if the file was downloaded successfully
if (blobData[0]) {
return [
true,
{ status_code: 200, status_msg: `read object [${fileName}]` },
blobData[1]
]
// In this case, the error is due to unescaped characters in the URL and we need to re-encode the file name
} else {
// Put an if statement here to check if the error is due to unescaped characters by checking the error message
if (blobData[1] === 'ERR_UNESCAPED_CHARACTERS') {
downloadUrl = reEncodeDownloadUrl(downloadUrl, originalFileNameEncoded)
// Try to download the file from the repository again
blobData = await downloadFile(downloadUrl)
if (blobData[0]) {
return [
true,
{ status_code: 200, status_msg: `read object [${fileName}]` },
blobData[1]
]
}
}
}
return [
false,
{ status_code: 503, status_msg: `unable to read object [${fileName}] due to [${blobData[1]}].` },
blobData[1]
]
}
// async readBlob(fileName) {
// // Encode the file name and obtain the download URL
// const encodedFileName = encodeURIComponent(fileName)
// // Set the object URL
// const objectUrl = `https://api.github.com/repos/${this.orgName}/${this.repoName}/contents/${encodedFileName}`
// // Set the headers
// const headers = { 'Authorization': `token ${this.token}` }
// // Obtain the download URL
// const result = await axios.get(objectUrl, { headers })
// console.log(result)
// let downloadUrl = result.data.download_url
// // try {
// const downloadResult = await axios.get(downloadUrl, { responseType: 'arraybuffer' })
// // console.log(downloadResult)
// return [true, downloadResult, downloadUrl]
// // } catch (e) {
// // console.log(e)
// // return [false, e, downloadUrl]
// // }
// }
// Create a method using the octokit called deleteBlob to delete a file from the repo
async deleteBlob(containerName, fileName, branchName, sha) {
// Using the github API delete a file from the container
try {
const deleteResponse = await this.octCtl.rest.repos.deleteFile({
owner: this.orgName,
repo: this.repoName,
path: `${containerName}/${fileName}`,
branch: branchName,
message: `Delete object [${fileName}]`,
sha: sha
})
// Return the delete response if the delete was successful or an error if not
return [true, {status_code: 200, status_msg: `deleted object [${fileName}] from container [${containerName}]`}, deleteResponse]
} catch (err) {
// Return the error
return [false, {status_code: 503, status_msg: `unable to delete object [${fileName}] from container [${containerName}]`}, err]
}
}
// Create a method using the octokit to write a file to the repo
async writeBlob(containerName, fileName, blob, branchName, sha) {
// Only pull in the file name
const fileBits = fileName.split('/')
const shortFilename = fileBits[fileBits.length - 1]
// Using the github API write a file to the container
let octoObj = {
owner: this.orgName,
repo: this.repoName,
path: `${containerName}/${shortFilename}`,
message: `Create object [${shortFilename}]`,
content: blob,
branch: branchName
}
if(sha) {
octoObj.sha = sha
}
try {
const writeResponse = await this.octCtl.rest.repos.createOrUpdateFileContents(octoObj)
// Return the write response if the write was successful or an error if not
return [true, `SUCCESS: wrote object [${fileName}] to container [${containerName}]`, writeResponse]
} catch (err) {
// Return the error
return [false, `ERROR: unable to write object [${fileName}] to container [${containerName}]`, err]
}
}
/**
* @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.
* @returns {Promise<string>} A promise that resolves to the response from the GitHub API.
* @throws {Error} If an error occurs while writing the object.
* @memberof GitHubFunctions
* @todo Add a check to see if the container is locked and if so return an error.
*/
async writeObject(containerName, obj, ref, mySha) {
// Using the github API write a file to the container
try {
const writeResponse = await this.octCtl.rest.repos.createOrUpdateFileContents({
owner: this.orgName,
repo: this.repoName,
path: `${containerName}/${this.objectFiles[containerName]}`,
message: `Create object [${this.objectFiles[containerName]}]`,
content: Buffer.from(JSON.stringify(obj)).toString('base64'),
branch: ref,
sha: mySha
})
// Return the write response if the write was successful or an error if not
return [true, `SUCCESS: wrote object [${this.objectFiles[containerName]}] to container [${containerName}]`, writeResponse]
} catch (err) {
// Return the error
return [false, `ERROR: unable to write object [${this.objectFiles[containerName]}] to container [${containerName}]`, err]
}
}
/**
* @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<string>} A promise that resolves to the decoded contents of the objects.
* @throws {Error} If an error occurs while getting the content or parsing it.
* @memberof GitHubFunctions
*/
async readObjects(containerName) {
// Using the GitHub API get the contents of a file
try {
let objectContents = await this.octCtl.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
ref: this.mainBranchName,
path: `${containerName}/${this.objectFiles[containerName]}`
})
// Decode the contents
const decodedContents = Buffer.from(objectContents.data.content, 'base64').toString()
// Parse the contents
objectContents.mrJson = JSON.parse(decodedContents)
// Return the contents
return [true, `SUCCESS: read and returned [${containerName}/${this.objectFiles[containerName]}]`, objectContents]
} catch (err) {
// Return the error
return [false, `ERROR: unable to read [${containerName}/${this.objectFiles[containerName]}]`, err]
}
}
/**
* @function updateObject
* @description Reads an object from a specified container using the GitHub API.
* @async
* @param {string} containerName - The name of the container to read the object from.
* @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 to the container or not.
* @param {boolean} [system=false] - A flag to indicate if the update is a system call or not.
* @param {Array} [whiteList=[]] - A list of keys that are allowed to be updated.
* @returns {Promise<Array>} A promise that resolves to the decoded contents of the object.
* @memberof GitHubFunctions
* @todo As deleteObject progresses look to see if we can improve here too
*/
async updateObject(containerName, objName, key, value, dontWrite=false, system=false, whiteList=[]) {
// console.log(`Updating object [${objName}] in container [${containerName}] with key [${key}] and value [${value}]`)
// Check to see if this is a system call or not
if(!system) {
// Since this is not a system call check to see if the key is in the white list
if(!whiteList.includes(key)) {
return [
false,
{
status_code: 403,
status_msg: `Updating the key [${key}] is not supported.`
},
null
]
}
}
// Using the method above read the objects
const readResponse = await this.readObjects(containerName)
// Check to see if the read was successful
if(!readResponse[0]) {
return [
false,
{status_code: 500, status_msg: `Unable to read source objects from GitHub.`},
readResponse
]
}
// Catch the container if needed
let repoMetadata = {
containers: {},
branch: {}
}
// If dontWrite is true then don't catch the container
let caught = {}
if(!dontWrite) {
repoMetadata.containers[containerName] = {}
caught = await this.catchContainer(repoMetadata)
}
// Loop through the objects, find and update the objects matching the name
for (const obj in readResponse[2].mrJson) {
if(readResponse[2].mrJson[obj].name === objName) {
readResponse[2].mrJson[obj][key] = value
// Update the modified date of the object
const now = new Date()
readResponse[2].mrJson[obj].modification_date = now.toISOString()
}
}
// If this flag is set merely return the modified object(s) to the caller
if (dontWrite) {
return [
true,
{
status_code: 200,
status_msg: `Merged updates object(s) with [${containerName}] objects.`
},
readResponse[2].mrJson
]
}
// Call the method above to write the object
const writeResponse = await this.writeObject(
containerName,
readResponse[2].mrJson,
caught[2].branch.name,
caught[2].containers[containerName].objectSha)
// Check to see if the write was successful and return the error if not
if(!writeResponse[0]) {
return [
false,
{status_code: 503, status_msg: `Unable to write the objects.`},
writeResponse
]
}
// Release the container
const released = await this.releaseContainer(caught[2])
if(!released[0]) {
return [
false,
{
status_code: 503,
status_msg: `Cannot release the container please check [${containerName}] in GitHub.`
},
released
]
}
// Finally return success with the results of the release
return [
true,
{
status_code: 200,
status_msg: `Updated [${containerName}] object of the name [${objName}] with [${key} = ${value}].`
},
released
]
}
/**
* @function deleteObject
* @description Deletes an object from a specified container using the GitHub API.
* @async
* @param {string} objName - The name of the object to delete.
* @param {object} source - The source object that contains the from and to containers.
* @returns {Promise<Array>} A promise that resolves to the decoded contents of the object.
* @memberof GitHubFunctions
*/
async deleteObject(objName, source, repoMetadata=null, catchIt=true) {
// NOTE: source has a weakness we will have to remedy later, notably from can be Studies and Companies
// source = {
// from: 'Interactions',
// to: ['Companies']
// }
// Create an object that maps the from object type to the to object type fields to be updated
const fieldMap = {
Interactions: {
Companies: 'linked_interactions',
// Studies: 'linked_interactions'
},
Companies: {
Interactions: 'linked_companies',
// Studies: 'linked_companies'
},
Studies: {
Interactions: 'linked_studies',
Companies: 'linked_studies'
}
}
// Catch the container if needed
if(catchIt) {
repoMetadata = {
containers: {},
branch: {}
}
// Catch the container(s)
repoMetadata.containers[source.from] = {}
repoMetadata.containers[source.to[0]] = {}
let caught = await this.catchContainer(repoMetadata)
repoMetadata = caught[2]
}
// Loop through the from objects, find and remove the objects matching the name
for (const obj in repoMetadata.containers[source.from].objects) {
if(repoMetadata.containers[source.from].objects[obj].name === objName) {
// If from is Interactions then we need to delete the actual file from the repo based on objName
if(source.from === 'Interactions') {
// Obtain the sha of the object to delete by obtaining the file name from the url attribute of the Interaction
const fileName = repoMetadata.containers[source.from].objects[obj].url
// Obtain the sha for fileName using octokit
const { data } = await this.octCtl.rest.repos.getContent({
owner: this.orgName,
repo: this.repoName,
path: fileName
})
// Remove the path from the file name
const fileBits = fileName.split('/')
const shortFilename = fileBits[fileBits.length - 1]
// Call the method above to delete the object using data.sha
const deleteResponse = await this.deleteBlob(
source.from,
shortFilename,
repoMetadata.branch.name,
data.sha
)
// Check to see if the delete was successful and return the error if not
if(!deleteResponse[0]) {
return [
false,
{status_code: 503, status_msg: `Unable to delete the [${source.from}] object [${objName}].`},
deleteResponse
]
}
}
// Remove the object from the array
repoMetadata.containers[source.from].objects.splice(obj, 1)
}
}
// Loop through the to objects, find and remove objName from the linked objects and update the modification date
for (const obj in repoMetadata.containers[source.to[0]].objects) {
if(objName in repoMetadata.containers[source.to[0]].objects[obj][fieldMap[source.from][source.to[0]]]) {
// Delete the object from the linked objects object
delete repoMetadata.containers[source.to[0]].objects[obj][fieldMap[source.from][source.to[0]]][objName]
// Update the modification date of the object
const now = new Date()
repoMetadata.containers[source.to[0]].objects[obj].modification_date = now.toISOString()
}
}
// Call getSha to get the sha of the from object
const fromSha = await this.getSha(source.from, this.objectFiles[source.from], repoMetadata.branch.name)
// Check to see if the sha was captured and return the error if not
if(!fromSha[0]) {
return [
false,
{status_code: 503, status_msg: `Unable to capture the [${source.from}] sha.`},
fromSha
]
}
// Call the method above to write the from objects
const writeResponse = await this.writeObject(
source.from,
repoMetadata.containers[source.from].objects,
repoMetadata.branch.name,
fromSha[2])
// Check to see if the write was successful and return the error if not
if(!writeResponse[0]) {
console.log(writeResponse)
return [
false,
{status_code: 503, status_msg: `Unable to write the [${source.from}] objects.`},
writeResponse
]
}
// Call getSha to get the sha of the to object
const toSha = await this.getSha(source.to[0], this.objectFiles[source.to[0]], repoMetadata.branch.name)
// Check to see if the sha was captured and return the error if not
if(!toSha[0]) {
return [
false,
{status_code: 503, status_msg: `Unable to capture the [${source.to[0]}] sha.`},
toSha
]
}
// Call the method above to write the to objects
const writeResponse2 = await this.writeObject(
source.to,
repoMetadata.containers[source.to[0]].objects,
repoMetadata.branch.name,
toSha[2])
// Check to see if the write was successful and return the error if not
if(!writeResponse2[0]) {
return [
false,
{status_code: 503, status_msg: `Unable to write the [${source.to}] objects.`},
writeResponse2
]
}
// Release the container
if(catchIt){
const released = await this.releaseContainer(repoMetadata)
if(!released[0]) {
return [
false,
{
status_code: 503,
status_msg: `Cannot release the container please check [${source.from}] in GitHub.`
},
released
]
}
// Finally return success with the results of the release
return [
true,
{
status_code: 200,
status_msg: `Deleted [${source.from}] object of the name [${objName}], and links in associated objects.`
},
released
]
} else {
// Return success with the write reponses
return [
true,
{
status_code: 200,
status_msg: `Deleted [${source.from}] object of the name [${objName}], and links in associated objects.`
},
[writeResponse, writeResponse2]
]
}
}
/**
* @function catchContainer
* @description Catches a container by locking it, creating a new branch, reading the objects, and returning the metadata.
* @param {Object} repoMetadata - The metadata object that contains the containers and branch information.
* @returns {Promise<Array>} A promise that resolves to an array containing a boolean indicating success, a success message, and the metadata object.
* @memberof GitHubFunctions
*/
async catchContainer(repoMetadata) {
// Check to see if the containers are locked
for (const container in repoMetadata.containers) {
// Call the method above to check for a lock
const lockExists = await this.checkForLock(container)
// If the lock exists return an error
if(lockExists[0]) { return [false, {status_code: 503, status_msg:`the container [${container}] is locked unable and cannot perform creates, updates or deletes on objects.`}, lockExists] }
}
// Lock the containers
for (const container in repoMetadata.containers) {
// Call the method above to lock the container
const locked = await this.lockContainer(container)
// Check to see if the container was locked and return the error if not
if(!locked[0]) { return [false, {status_code: 503, status_msg: `unable to lock [${container}] and cannot perform creates, updates or deletes on objects.`}, locked] }
// Save the lock sha
repoMetadata.containers[container].lockSha = locked[2].data.content.sha
}
// Call the method above createBranchFromMain to create a new branch
const branchCreated = await this.createBranchFromMain()
// Check to see if the branch was created
if(!branchCreated[0]) { return [false, {status_code: 503, status_msg: `unable to create new branch`}, branchCreated] }
// Save the branch sha into containers as a separate object
repoMetadata.branch = {
name: branchCreated[2].data.ref,
sha: branchCreated[2].data.object.sha
}
// Read the objects from the containers
for (const container in repoMetadata.containers) {
// Call the method above to read the objects
const readResponse = await this.readObjects(container)
// Check to see if the read was successful
if(!readResponse[0]) { return [false, {status_code: 503, status_msg: `Unable to read the source objects [${container}/${this.objectFiles[container]}].`}, readResponse] }
// Save the object sha into containers as a separate object
repoMetadata.containers[container].objectSha = readResponse[2].data.sha
// Save the objects into containers as a separate object
repoMetadata.containers[container].objects = readResponse[2].mrJson
}
return [true,{status_code: 200, status_msg: `${repoMetadata.containers.length} containers are ready for use.`}, repoMetadata]
}
/**
* @function releaseContainer
* @description Releases a container by unlocking it and merging the branch to main.
* @param {Object} repoMetadata - The metadata object that contains the containers and branch information.
* @returns {Promise<Array>} A promise that resolves to an array containing a boolean indicating success, a success message, and the response from the GitHub API.
* @memberof GitHubFunctions
*/
async releaseContainer(repoMetadata) {
// Merge the branch to main
const mergeResponse = await this.mergeBranchToMain(repoMetadata.branch.name, repoMetadata.branch.sha)
// Check to see if the merge was successful and return the error if not
if(!mergeResponse[0]) { return [false,{status_code:503, status_msg: `Unable to merge the branch to main.`}, mergeResponse] }
// Unlock the containers by looping through them
for (const container in repoMetadata.containers) {
// Call the method above to unlock the container
const branchUnlocked = await this.unlockContainer(
container,
repoMetadata.containers[container].lockSha,
repoMetadata.branch.name)
if(!branchUnlocked[0]) { return [false, {status_code: 503, status_msg: `Unable to unlock the container, objects may have been written please check [${container}] for objects and the lock file.`}, branchUnlocked] }
// Unlock main
const mainUnlocked = await this.unlockContainer(
container,
repoMetadata.containers[container].lockSha
)
if(!mainUnlocked[0]) { return [false, {status_code: 503, status_msg: `Unable to unlock the container, objects may have been written please check [${contai