/** * A class for authenticating and talking to the mediumroast.io backend * @author Michael Hay <michael.hay@mediumroast.io> * @file gitHubServer.js * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 * @version 2.0.0 * * @class baseObjects * @classdesc An implementation for interacting with the GitHub backend. * * @requires GitHubFunctions * @requires crypto * @requires fs * @requires path * @requires fileURLToPath * * @exports {Studies, Companies, Interactions, Users, Storage, Actions} * * @example * import {Companies, Interactions, Users, Billings} from './api/gitHubServer.js' * const companies = new Companies(token, org, processName) * const interactions = new Interactions(token, org, processName) * const users = new Users(token, org, processName) * const billings = new Billings(token, org, processName) * * const allCompanies = await companies.getAll() * const allInteractions = await interactions.getAll() * const allUsers = await users.getAll() * const allBillings = await billings.getAll() * * const company = await companies.findByName('myCompany') * const interaction = await interactions.findByName('myInteraction') * const user = await users.findByName('myUser') * */ // Import required modules import GitHubFunctions from './github.js' import { createHash } from 'crypto' import fs from 'fs' import * as path from 'path' import { fileURLToPath } from 'url' class baseObjects { constructor(token, org, processName, objType) { this.serverCtl = new GitHubFunctions(token, org, processName) this.objType = objType this.objectFiles = { Studies: 'Studies.json', Companies: 'Companies.json', Interactions: 'Interactions.json', Users: null } } /** * @async * @function getAll * @description Get all objects from the mediumroast.io application * @returns {Array} the results from the called function mrRest class */ async getAll() { return await this.serverCtl.readObjects(this.objType) } /** * @async * @function findByName * @description Find all objects by name from the mediumroast.io application * @param {String} name - the name of the object to find * @returns {Array} the results from the called function mrRest class */ async findByName(name) { return this.findByX('name', name) } /** * @async * @function findById * @description Find all objects by id from the mediumroast.io application * @param {String} id - the id of the object to find * @param {String} endpoint - defaults to findbyx and is combined with credential and version info * @returns {Array} the results from the called function mrRest class * @deprecated */ async findById(id) { return false const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint const my_obj = {findByX: "id", xEquals: id} return this.rest.postObj(fullEndpoint, my_obj) } /** * @async * @function findByX * @description Find all objects by attribute and value pair from the mediumroast.io application * @param {String} attribute - the attribute used to find objects * @param {String} value - the value for the defined attribute * @returns {Array} the results from the called function mrRest class */ async findByX(attribute, value, allObjects=null) { if(attribute === 'name') { value = value.toLowerCase() } // console.log(`Searching for ${this.objType} where ${attribute} = ${value}`) let myObjects = [] if(allObjects === null) { const allObjectsResp = await this.serverCtl.readObjects(this.objType) allObjects = allObjectsResp[2].mrJson } // If the length of allObjects is 0 then return an error // This will occur when there are no objects of the type in the backend if(allObjects.length === 0) { return [false, {status_code: 404, status_msg: `no ${this.objType} found`}, null] } for(const obj in allObjects) { let currentObject attribute == 'name' ? currentObject = allObjects[obj][attribute].toLowerCase() : currentObject = allObjects[obj][attribute] if(currentObject === value) { myObjects.push(allObjects[obj]) } } if (myObjects.length === 0) { return [false, {status_code: 404, status_msg: `no ${this.objType} found where ${attribute} = ${value}`}, null] } else { return [true, `SUCCESS: found all objects where ${attribute} = ${value}`, myObjects] } } /** * @async * @function createObj * @description Create objects in the mediumroast.io application * @param {Array} objs - the objects to create in the backend * @returns {Array} the results from the called function mrRest class */ // async createObj1(objs) { // return await this.serverCtl.createObjects(this.objType, objs) // } async createObj(objs) { // Create the repoMetadata object let repoMetadata = { containers: { [this.objType]: {} }, branch: {} } // Catch the container const caught = await this.serverCtl.catchContainer(repoMetadata) // If the container is locked then return the caught object if(!caught[0]) { return caught } // Get the sha for the current branch/object const sha = await this.serverCtl.getSha( this.objType, this.objectFiles[this.objType], repoMetadata.branch.name ) // If the sha is not found then return the sha object if(!sha[0]) { return sha } // Append the new object to the existing objects const mergedObjects = [...caught[2].containers[this.objType].objects, ...objs] // Write the new objects to the container const writeResp = await this.serverCtl.writeObject( this.objType, mergedObjects, repoMetadata.branch.name, sha[2] ) // If the write fails then return the writeResp if(!writeResp[0]) { return writeResp } // Release the container const released = await this.serverCtl.releaseContainer(caught[2]) // If the release fails then return the released object if(!released[0]) { return released } // Return a success message return [true, {status_code: 200, status_msg: `created [${objs.length}] ${this.objType}`}, null] } /** * @async * @function updateObj * @description Update an object in the mediumroast.io application * @param {Object} obj - the object to update in the backend which includes the id and, the attribute and value to be updated * @param {String} endpoint - defaults to findbyx and is combined with credential and version info * @returns {Array} the results from the called function mrRest class */ async updateObj(objName, key, value, dontWrite, system, whiteList) { return await this.serverCtl.updateObject(this.objType, objName, key, value, dontWrite, system, whiteList) } /** * @async * @function deleteObj * @description Delete an object in the mediumroast.io application * @param {String} id - the object to be deleted in the mediumroast.io application * @param {String} endpoint - defaults to findbyx and is combined with credential and version info * @returns {Array} the results from the called function mrRest class * @todo implment when available in the backend */ async deleteObj(objName, source, repoMetadata=null, catchIt=true) { return await this.serverCtl.deleteObject(objName, source, repoMetadata, catchIt) } /** * @async * @function linkObj * @description Link objects in the mediumroast.io application * @param {Array} objs - the objects to link in the backend * @returns {Array} the results from the called function mrRest class */ linkObj(objs) { let linkedObjs = {} for(const obj in objs) { const objName = objs[obj].name const sha256Hash = createHash('sha256').update(objName).digest('hex') linkedObjs[objName] = sha256Hash } return linkedObjs } // Create a function that checks for a locked container using the serverCtl.checkForLock() function async checkForLock() { return await this.serverCtl.checkForLock(this.objType) } } class Studies extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the study objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Studies') } } // Create a subclass called Users that inherits from baseObjects class Users extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the user objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Users') } // Create a new method for getAll that is specific to the Users class using getUser() in github.js async getAll() { return await this.serverCtl.getAllUsers() } // Create a new method for findMyself that is specific to the Users class using getUser() in github.js async getMyself() { return await this.serverCtl.getUser() } async findByName(name) { return this.findByX('login', name) } async findByX(attribute, value) { let myUsers = [] const allUsersResp = await this.getAll() const allUsers = allUsersResp[2] for(const user in allUsers) { if(allUsers[user][attribute] === value) { myUsers.push(allUsers[user]) } } return [true, `SUCCESS: found all users where ${attribute} = ${value}`, myUsers] } } // Create a subclass called Users that inherits from baseObjects class Storage extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the user objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Billings') } // Create a new method for getAll that is specific to the Billings class using getBillings() in github.js async getAll() { return await this.serverCtl.getRepoSize() } // Create a new method of to get the storage billing status only async getStorageBilling() { return await this.serverCtl.getStorageBillings() } } class Companies extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the company objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Companies') } async updateObj(objToUpdate, dontWrite=false, system=false) { // Destructure objToUpdate const { name, key, value } = objToUpdate // Define the attributes that can be updated by the user const whiteList = [ 'description', 'company_type', 'url', 'role', 'wikipedia_url', 'status', 'logo_url', 'region', 'country', 'city', 'state_province', 'zip_postal', 'street_address', 'latitude', 'longitude','phone', 'google_maps_url', 'google_news_url', 'google_finance_url','google_patents_url', 'cik', 'stock_symbol', 'stock_exchange', 'recent_10k_url', 'recent_10q_url', 'firmographic_url', 'filings_url', 'owner_tranasactions', 'industry', 'industry_code', 'industry_group_code', 'industry_group_description', 'major_group_code','major_group_description' ] return await super.updateObj(name, key, value, dontWrite, system, whiteList) } async deleteObj(objName, allowOrphans=false) { let source = { from: 'Companies', to: ['Interactions'] } // If allowOrphans is true then use the baseObjects deleteObj if(allowOrphans){ return await super.deleteObj(objName, source) } // Catch the Companies and Interaction containers // Assign repoMetadata to capture Companies nad Studies let repoMetadata = { containers: { Companies: {}, Interactions: {} }, branch: {} } const caught = await this.serverCtl.catchContainer(repoMetadata) // Use findByX to get all linkedInteractions // NOTE: This has to be done here because the company has been deleted in the next step const getCompanyObject = await this.findByX('name', objName, caught[2].containers.Companies.objects) if(!getCompanyObject[0]) { return getCompanyObject } const linkedInteractions = getCompanyObject[2][0].linked_interactions // Delete the company // Use deleteObj to delete the company const deleteCompanyObjResp = await this.serverCtl.deleteObject( objName, source, caught[2], false ) if(!deleteCompanyObjResp[0]) { return deleteCompanyObjResp } // Delete all linkedInteractions // Update source to be from the perspective of the Interactions source = { from: 'Interactions', to: ['Companies'] } // Use deleteObect to delete all linkedInteractions for(const interaction in linkedInteractions) { const deleteInteractionObjResp = await this.serverCtl.deleteObject( interaction, source, caught[2], false ) if(!deleteInteractionObjResp[0]) { return deleteInteractionObjResp } } // Release the container const relased = await this.serverCtl.releaseContainer(caught[2]) if(!relased[0]) { return relased } // Return the response return [true, {status_code: 200, status_msg: `deleted company [${objName}] and all linked interactions`}, null] } } class Interactions extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the interaction objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Interactions') } async updateObj(objToUpdate, dontWrite=false, system=false) { // Destructure objToUpdate const { name, key, value } = objToUpdate // Define the attributes that can be updated by the user const whiteList = [ 'status', 'content_type', 'file_size', 'reading_time', 'word_count', 'page_count', 'description', 'abstract', 'region', 'country', 'city', 'state_province', 'zip_postal', 'street_address', 'latitude', 'longitude', 'public', 'groups' ] return await super.updateObj(name, key, value, dontWrite, system, whiteList) } async deleteObj(objName) { const source = { from: 'Interactions', to: ['Companies'] } return await super.deleteObj(objName, source) } async findByHash(hash) { return this.findByX('file_hash', hash) } } class Actions extends baseObjects { /** * @constructor * @classdesc A subclass of baseObjects that construct the interaction objects * @param {String} token - the token for the GitHub application * @param {String} org - the organization for the GitHub application * @param {String} processName - the process name for the GitHub application */ constructor (token, org, processName) { super(token, org, processName, 'Actions') } _generateManifest(dir, filelist) { // Define which content to skip const skipContent = ['.DS_Store', 'node_modules'] const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename); // Use regex to prune everything after mediumroast_js/ const basePath = __dirname.match(/.*mediumroast_js\//)[0]; // Append cli/actions to the base path dir = dir || path.resolve(path.join(basePath, 'cli/actions')) const files = fs.readdirSync(dir) filelist = filelist || []; files.forEach((file) => { // Skip unneeded directories if (skipContent.includes(file)) { return } const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { filelist = this._generateManifest(path.join(dir, file), filelist) } else { // Substitute .github for the first part of the path, in the variable dir if (dir.includes('./')) { dir = dir.replace('./', '') } // This will be the repository name let dotGitHub = dir.replace(/.*(workflows|actions)/, '.github/$1') filelist.push({ fileName: file, containerName: dotGitHub, srcURL: new URL(`file://${fullPath}`) }) } }) return filelist } async updateActions() { // Discover the manifest const actionsManifest = this._generateManifest() // Capture detailed install status let installStatus = { successCount: 0, failCount: 0, success: [], fail: [], total: actionsManifest.length } for (const action of actionsManifest) { // Loop through the actionsManifest and install each action let status = false let blobData try { // Read in the blob file blobData = fs.readFileSync(action.srcURL, 'base64') status = true } catch (err) { // console.log(`Unable to read file [${action.fileName}] because: ${err}`) return [false,{status_code: 500, status_msg: `Unable to read file [${action.fileName}] because: ${err}`}, installStatus] } if(status) { // Get the sha for the current branch/object const sha = await this.serverCtl.getSha( action.containerName, action.fileName, 'main' ) // Keep action update failures // Install the action const installResp = await this.serverCtl.writeBlob( action.containerName, action.fileName, blobData, 'main', sha[2] ) if(installResp[0]){ installStatus.success.push({fileName: action.fileName, containerName: action.catchContainer, installMsg: installResp[1].status_msg}) installStatus.successCount++ } else { installStatus.fail.push({fileName: action.fileName, containerName: action.catchContainer, installMsg: installResp[1].status_msg}) installStatus.failCount++ } } else { return [false, {status_code: 503,status_msg:`Failed to read item [${action.fileName}]`}, installStatus] } } return [true, {status_code: 200, status_msg:`All actions installed`}, installStatus] } // Create a new method of to get the actions billing status only async getActionsBilling() { return await this.serverCtl.getActionsBillings() } async getAll() { return await this.serverCtl.getWorkflowRuns() } } // Export classes for consumers export { Studies, Companies, Interactions, Users, Storage, Actions }