diff --git a/cli/backup.js b/cli/backup.js index fb89c45..2ec113d 100755 --- a/cli/backup.js +++ b/cli/backup.js @@ -140,6 +140,26 @@ function restoreObjects (fileName, apiController) { } else { console.error("ERROR (%d): " + msg, -1) } + /* + const [success, msg, rawData] = myCLI.readTextFile(myArgs.create) + if (success) { + const jsonData = JSON.parse(rawData) + const toRegister = jsonData.map(async element => { + const [success, stat, resp] = await apiController.createObj(element) + if (await stat.status_code == 200) { + console.log(`SUCCESS: Created new [${objectType}] object in the mediumroast.io backend.`) + } else { + console.error('ERROR (%d): ' + stat.status_msg, stat.status_code) + } + }) + const registered = await Promise.all(toRegister) + console.log(`SUCCESS: Loaded [${jsonData.length}] objects from file [${myArgs.create}].`) + process.exit(0) + } else { + console.error("ERROR (%d): " + msg, -1) + process.exit(-1) + } + */ } // Business end of the CLI diff --git a/cli/company.js b/cli/company.js index aebb10a..c72ed72 100755 --- a/cli/company.js +++ b/cli/company.js @@ -6,45 +6,67 @@ * @file company.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 2.0.0 + * @version 2.2.0 */ // Import required modules -import { Auth, Companies, Interactions } from '../src/api/mrServer.js' -import { Utilities } from '../src/helpers.js' -import { CLIUtilities } from '../src/cli.js' import { CompanyStandalone } from '../src/report/companies.js' import AddCompany from '../src/cli/companyWizard.js' +import Environmentals from '../src/cli/env.js' +import s3Utilities from '../src/cli/s3.js' +import CLIOutput from '../src/cli/output.js' +import FilesystemOperators from '../src/cli/filesystem.js' +import serverOperations from '../src/cli/common.js' +import ArchivePackage from '../src/cli/archive.js' -// Globals +// External modules +import chalk from 'chalk' + +// Related object type const objectType = 'company' -// Construct the CLI object -const myCLI = new CLIUtilities ( +// Environmentals object +const environment = new Environmentals( '2.0', - 'company', - 'Command line interface for mediumroast.io Company objects.', + `${objectType}`, + `Command line interface for mediumroast.io ${objectType} objects.`, objectType ) -// Construct the Utilities object -const utils = new Utilities(objectType) +// Filesystem object +const fileSystem = new FilesystemOperators() // Create the environmental settings -const myArgs = myCLI.parseCLIArgs() -const myConfig = myCLI.getConfig(myArgs.conf_file) -const myEnv = myCLI.getEnv(myArgs, myConfig) - -// Generate the credential & construct the API Controller -const myAuth = new Auth( - myEnv.restServer, - myEnv.apiKey, - myEnv.user, - myEnv.secret -) -const myCredential = myAuth.login() -const apiController = new Companies(myCredential) -const interactionController = new Interactions(myCredential) +const myArgs = environment.parseCLIArgs() +const myConfig = environment.getConfig(myArgs.conf_file) +const myEnv = environment.getEnv(myArgs, myConfig) + +// Output object +const output = new CLIOutput(myEnv, objectType) + +// S3 object +const s3 = new s3Utilities(myEnv) + +// Common server ops and also check the server +const serverOps = new serverOperations(myEnv) +// Checking to see if the server is ready for operations +const serverReady = await serverOps.checkServer() +if(serverReady[0]) { + console.log( + chalk.red.bold( + `No objects detected on your mediumroast.io server [${myEnv.restServer}].\n` + + `Perhaps you should try to run mr_setup first to create the owning company, exiting.` + ) + ) + process.exit(-1) +} + +// Assign the controllers based upon the available server +const companyCtl = serverReady[2].companyCtl +const interactionCtl = serverReady[2].interactionCtl +const studyCtl = serverReady[2].studyCtl +const owningCompany = await serverOps.getOwningCompany(companyCtl) +const sourceBucket = s3.generateBucketName(owningCompany[2]) // Predefine the results variable let [success, stat, results] = [null, null, null] @@ -52,12 +74,12 @@ let [success, stat, results] = [null, null, null] // Process the cli options if (myArgs.report) { // Retrive the interaction by Id - const [comp_success, comp_stat, comp_results] = await apiController.findById(myArgs.report) + const [comp_success, comp_stat, comp_results] = await companyCtl.findById(myArgs.report) // Retrive the company by Name const interactionNames = Object.keys(comp_results[0].linked_interactions) let interactions = [] for (const interactionName in interactionNames) { - const [mySuccess, myStat, myInteraction] = await interactionController.findByName( + const [mySuccess, myStat, myInteraction] = await interactionCtl.findByName( interactionNames[interactionName] ) interactions.push(myInteraction[0]) @@ -76,11 +98,10 @@ if (myArgs.report) { 'mediumroast.io barrista robot', // The author 'Mediumroast, Inc.' // The authoring company/org ) - if(myArgs.package) { // Create the working directory - const [dir_success, dir_msg, dir_res] = utils.safeMakedir(baseDir + '/interactions') + const [dir_success, dir_msg, dir_res] = fileSystem.safeMakedir(baseDir + '/interactions') // If the directory creations was successful download the interaction if(dir_success) { @@ -94,7 +115,7 @@ if (myArgs.report) { access points, but the tradeoff would be that caffeine would need to run on a system with file system access to these objects. */ - await utils.s3DownloadObjs(interactions, myEnv, baseDir + '/interactions') + await s3.s3DownloadObjs(interactions, baseDir + '/interactions', sourceBucket) // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) @@ -107,16 +128,15 @@ if (myArgs.report) { // Create the package and cleanup as needed if (myArgs.package) { - const [package_success, package_stat, package_result] = await utils.createZIPArchive( - myEnv.outputDir + '/' + baseName + '.zip', - baseDir - ) + const archiver = new ArchivePackage(myEnv.outputDir + '/' + baseName + '.zip') + const [package_success, package_stat, package_result] = await archiver.createZIPArchive(baseDir) if (package_success) { console.log(package_stat) - utils.rmDir(baseDir) + fileSystem.rmDir(baseDir) process.exit(0) } else { console.error(package_stat, -1) + fileSystem.rmDir(baseDir) process.exit(-1) } @@ -131,35 +151,18 @@ if (myArgs.report) { process.exit(-1) } } else if (myArgs.find_by_id) { - [success, stat, results] = await apiController.findById(myArgs.find_by_id) + [success, stat, results] = await companyCtl.findById(myArgs.find_by_id) } else if (myArgs.find_by_name) { - [success, stat, results] = await apiController.findByName(myArgs.find_by_name) + [success, stat, results] = await companyCtl.findByName(myArgs.find_by_name) } else if (myArgs.find_by_x) { - const myCLIObj = JSON.parse(myArgs.find_by_x) - const toFind = Object.entries(myCLIObj)[0] - [success, stat, results] = await apiController.findByX(toFind[0], toFind[1]) -} else if (myArgs.create) { - const [success, msg, rawData] = myCLI.readTextFile(myArgs.create) - if (success) { - const jsonData = JSON.parse(rawData) - const toRegister = jsonData.map(async element => { - const [success, stat, resp] = await apiController.createObj(element) - if (await stat.status_code == 200) { - console.log(`SUCCESS: Created new [${objectType}] object in the mediumroast.io backend.`) - } else { - console.error('ERROR (%d): ' + stat.status_msg, stat.status_code) - } - }) - const registered = await Promise.all(toRegister) - console.log(`SUCCESS: Loaded [${jsonData.length}] objects from file [${myArgs.create}].`) - process.exit(0) - } else { - console.error("ERROR (%d): " + msg, -1) - process.exit(-1) - } + const [myKey, myValue] = Object.entries(JSON.parse(myArgs.find_by_x))[0] + const foundObjects = await companyCtl.findByX(myKey, myValue) + success = foundObjects[0] + stat = foundObjects[1] + results = foundObjects[2] } else if (myArgs.update) { const myCLIObj = JSON.parse(myArgs.update) - const [success, stat, resp] = await apiController.updateObj(myCLIObj) + const [success, stat, resp] = await companyCtl.updateObj(myCLIObj) if(success) { console.log(`SUCCESS: processed update to company object.`) process.exit(0) @@ -169,7 +172,7 @@ if (myArgs.report) { } } else if (myArgs.delete) { // Delete an object - const [success, stat, resp] = await apiController.deleteObj(myArgs.delete) + const [success, stat, resp] = await companyCtl.deleteObj(myArgs.delete) if(success) { console.log(`SUCCESS: deleted company object.`) process.exit(0) @@ -178,8 +181,8 @@ if (myArgs.report) { process.exit(-1) } } else if (myArgs.add_wizard) { - // pass in credential, apiController - const newCompany = new AddCompany(myEnv, apiController, myCredential, myCLI) + // pass in credential, companyCtl + const newCompany = new AddCompany(myEnv, companyCtl, myCredential) const result = await newCompany.wizard() if(result[0]) { console.log('SUCCESS: Created new company in the backend') @@ -189,8 +192,8 @@ if (myArgs.report) { process.exit(-1) } } else { - [success, stat, results] = await apiController.getAll() + [success, stat, results] = await companyCtl.getAll() } // Emit the output -myCLI.outputCLI(myArgs.output, results, myEnv, objectType) \ No newline at end of file +output.outputCLI(results, myArgs.output) \ No newline at end of file diff --git a/cli/interaction.js b/cli/interaction.js index c7977fd..c9e247d 100755 --- a/cli/interaction.js +++ b/cli/interaction.js @@ -6,55 +6,80 @@ * @file interactions.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 2.0.0 + * @version 2.2.0 */ // Import required modules -import { Auth, Interactions, Companies, Studies } from '../src/api/mrServer.js' -import { CLIUtilities } from '../src/cli.js' -import { Utilities } from '../src/helpers.js' import { InteractionStandalone } from '../src/report/interactions.js' -import { AddInteraction } from '../src/cli/interactionWizard.js' +import AddInteraction from '../src/cli/interactionWizard.js' +import Environmentals from '../src/cli/env.js' +import s3Utilities from '../src/cli/s3.js' +import CLIOutput from '../src/cli/output.js' +import FilesystemOperators from '../src/cli/filesystem.js' +import serverOperations from '../src/cli/common.js' +import ArchivePackage from '../src/cli/archive.js' -// Globals +// External modules +import chalk from 'chalk' + +// Related object type const objectType = 'interaction' -// Construct the CLI object -const myCLI = new CLIUtilities( +// Environmentals object +const environment = new Environmentals( '2.0', - 'interaction', - 'Command line interface for mediumroast.io Interaction objects.', + `${objectType}`, + `Command line interface for mediumroast.io ${objectType} objects.`, objectType ) -const utils = new Utilities(objectType) + +// Filesystem object +const fileSystem = new FilesystemOperators() // Create the environmental settings -const myArgs = myCLI.parseCLIArgs() -const myConfig = myCLI.getConfig(myArgs.conf_file) -const myEnv = myCLI.getEnv(myArgs, myConfig) - -// Generate the credential & construct the API Controller -const myAuth = new Auth( - myEnv.restServer, - myEnv.apiKey, - myEnv.user, - myEnv.secret -) -const myCredential = myAuth.login() -const apiController = new Interactions(myCredential) -const companyController = new Companies(myCredential) -const studyController = new Studies(myCredential) +const myArgs = environment.parseCLIArgs() +const myConfig = environment.getConfig(myArgs.conf_file) +const myEnv = environment.getEnv(myArgs, myConfig) + +// Output object +const output = new CLIOutput(myEnv, objectType) + +// S3 object +const s3 = new s3Utilities(myEnv) + +// Common server ops and also check the server +const serverOps = new serverOperations(myEnv) +// Checking to see if the server is ready for operations +const serverReady = await serverOps.checkServer() +if(serverReady[0]) { + console.log( + chalk.red.bold( + `No objects detected on your mediumroast.io server [${myEnv.restServer}].\n` + + `Perhaps you should try to run mr_setup first to create the owning company, exiting.` + ) + ) + process.exit(-1) +} + +// Assign the controllers based upon the available server +const companyCtl = serverReady[2].companyCtl +const interactionCtl = serverReady[2].interactionCtl +const studyCtl = serverReady[2].studyCtl +const owningCompany = await serverOps.getOwningCompany(companyCtl) +const sourceBucket = s3.generateBucketName(owningCompany[2]) // Predefine the results variable -let [success, stat, results] = [null, null, null] +let success = Boolean() +let stat = Object() || {} +let results = Array() || [] // Process the cli options if (myArgs.report) { // Retrive the interaction by Id - const [int_success, int_stat, int_results] = await apiController.findById(myArgs.report) + const [int_success, int_stat, int_results] = await interactionCtl.findById(myArgs.report) // Retrive the company by Name const companyName = Object.keys(int_results[0].linked_companies)[0] - const [comp_success, comp_stat, comp_results] = await companyController.findByName(companyName) + const [comp_success, comp_stat, comp_results] = await companyCtl.findByName(companyName) // Set the root name to be used for file and directory names in case of packaging const baseName = int_results[0].name.replace(/ /g,"_") // Set the directory name for the package @@ -70,10 +95,9 @@ if (myArgs.report) { 'Mediumroast, Inc.' // The authoring company/org ) - if(myArgs.package) { // Create the working directory - const [dir_success, dir_msg, dir_res] = utils.safeMakedir(baseDir + '/interactions') + const [dir_success, dir_msg, dir_res] = fileSystem.safeMakedir(baseDir + '/interactions') // If the directory creations was successful download the interaction if(dir_success) { @@ -87,7 +111,7 @@ if (myArgs.report) { access points, but the tradeoff would be that caffeine would need to run on a system with file system access to these objects. */ - await utils.s3DownloadObjs(int_results, myEnv, baseDir + '/interactions') + await s3.s3DownloadObjs(int_results, baseDir + '/interactions', sourceBucket) // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) @@ -100,16 +124,15 @@ if (myArgs.report) { // Create the package and cleanup as needed if (myArgs.package) { - const [package_success, package_stat, package_result] = await utils.createZIPArchive( - myEnv.outputDir + '/' + baseName + '.zip', - baseDir - ) + const archiver = new ArchivePackage(myEnv.outputDir + '/' + baseName + '.zip') + const [package_success, package_stat, package_result] = await archiver.createZIPArchive(baseDir) if (package_success) { console.log(package_stat) - utils.rmDir(baseDir) + fileSystem.rmDir(baseDir) process.exit(0) } else { console.error(package_stat, -1) + fileSystem.rmDir(baseDir) process.exit(-1) } @@ -126,38 +149,20 @@ if (myArgs.report) { } else if (myArgs.find_by_id) { // Retrive the interaction by Id - [success, stat, results] = await apiController.findById(myArgs.find_by_id) + [success, stat, results] = await interactionCtl.findById(myArgs.find_by_id) } else if (myArgs.find_by_name) { // Retrive the interaction by Name - [success, stat, results] = await apiController.findByName(myArgs.find_by_name) + [success, stat, results] = await interactionCtl.findByName(myArgs.find_by_name) } else if (myArgs.find_by_x) { // Retrive the interaction by attribute as specified by X - const myCLIObj = JSON.parse(myArgs.find_by_x) - const toFind = Object.entries(myCLIObj)[0] - [success, stat, results] = await apiController.findByX(toFind[0], toFind[1]) -} else if (myArgs.create) { - // Create objects as defined in a JSON file, see example_data/*.json for examples - const [success, msg, rawData] = myCLI.readTextFile(myArgs.create) - if (success) { - const jsonData = JSON.parse(rawData) - const toRegister = jsonData.map(async element => { - const [success, stat, resp] = await apiController.createObj(element) - if (await stat.status_code == 200) { - console.log(`SUCCESS: Created new [${objectType}] object in the mediumroast.io backend.`) - } else { - console.error('ERROR (%d): ' + stat.status_msg, stat.status_code) - } - }) - const registered = await Promise.all(toRegister) - console.log(`SUCCESS: Loaded [${jsonData.length}] objects from file [${myArgs.create}].`) - process.exit(0) - } else { - console.error("ERROR (%d): " + msg, -1) - process.exit(-1) - } + const [myKey, myValue] = Object.entries(JSON.parse(myArgs.find_by_x))[0] + const foundObjects = await interactionCtl.findByX(myKey, myValue) + success = foundObjects[0] + stat = foundObjects[1] + results = foundObjects[2] } else if (myArgs.update) { const myCLIObj = JSON.parse(myArgs.update) - const [success, stat, resp] = await apiController.updateObj(myCLIObj) + const [success, stat, resp] = await interactionCtl.updateObj(myCLIObj) if(success) { console.log(`SUCCESS: processed update to interaction object.`) process.exit(0) @@ -167,7 +172,7 @@ if (myArgs.report) { } } else if (myArgs.delete) { // Delete an object - const [success, stat, resp] = await apiController.deleteObj(myArgs.delete) + const [success, stat, resp] = await interactionCtl.deleteObj(myArgs.delete) if(success) { console.log(`SUCCESS: deleted interaction object.`) process.exit(0) @@ -176,25 +181,19 @@ if (myArgs.report) { process.exit(-1) } } else if (myArgs.add_wizard) { - // pass in credential, apiController, etc. - const myApiCtl = { - interaction: apiController, - company: companyController, - study: studyController - } - const newInteraction = new AddInteraction(myEnv, myApiCtl, myCredential, myCLI) + const newInteraction = new AddInteraction(myEnv) const result = await newInteraction.wizard() if(result[0]) { - console.log('SUCCESS: Created new interaction in the backend') + console.log('SUCCESS: Created new interactions in the backend') process.exit(0) } else { - console.error('ERROR: Failed to create interaction object with %d', result[1].status_code) + console.error('ERROR: Failed to create interaction objects with %d', result[1].status_code) process.exit(-1) } } else { // Get all objects - [success, stat, results] = await apiController.getAll() + [success, stat, results] = await interactionCtl.getAll() } // Emit the output -myCLI.outputCLI(myArgs.output, results, myEnv, objectType) \ No newline at end of file +output.outputCLI(results, myArgs.output) \ No newline at end of file diff --git a/cli/setup.js b/cli/setup.js index 369d4db..580e276 100755 --- a/cli/setup.js +++ b/cli/setup.js @@ -12,15 +12,14 @@ // Import required modules import { Utilities } from '../src/helpers.js' import { Auth, Companies, Interactions, Studies } from '../src/api/mrServer.js' -import CLIOutput from '../src/output.js' +import CLIOutput from '../src/cli/output.js' import WizardUtils from '../src/cli/commonWizard.js' import AddCompany from '../src/cli/companyWizard.js' -import s3Utilities from '../src/s3.js' +import s3Utilities from '../src/cli/s3.js' import program from 'commander' import chalk from 'chalk' import ConfigParser from 'configparser' -import crypto from "node:crypto" /* ----------------------------------------------------------------------- @@ -69,7 +68,7 @@ function getEnv () { api_key: "b7d1ac5ec5c2193a7d6dd61e7a8a76451885da5bd754b2b776632afd413d53e7", server: "http://cherokee.from-ca.com:9000", region: "leo-dc", - source: "Unknown" + source: "Unknown" // TODO this is deprecated remove after testing }, document_settings: { font_type: "Avenir Next", @@ -197,7 +196,6 @@ const utils = new Utilities("all") // Unless we suppress this print out the splash screen. if (myArgs.splash === 'yes') { - console.clear() // Attempt to clear the screen cliOutput.splashScreen( "mediumroast.io Setup Wizard", "version 2.0.0", @@ -313,7 +311,7 @@ cliOutput.outputCLI(myCompanies[2]) cliOutput.printLine() // Print out the next steps -console.log(chalk.blue.bold(`Now that you\'ve performed the initial registration here\'s what\'s next.`)) +console.log(`Now that you\'ve performed the initial registration here\'s what\'s next.`) console.log(chalk.blue.bold(`\t1. Create and register additional companies with mr_company --add_wizard.`)) console.log(chalk.blue.bold(`\t2. Register and add interactions with mr_interaction --add_wizard.`)) console.log('\nWith additional companies and new interactions registered the mediumroast.io caffeine\nservice will perform basic competitive analysis.') diff --git a/package.json b/package.json index 842afa1..0f76cbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.3.8", + "version": "0.3.15", "description": "A Javascript SDK to interact with mediumroast.io including command line interfaces.", "main": "src/api/mrServer.js", "scripts": { @@ -18,6 +18,7 @@ "product", "product management", "mediumroast", + "mediumroast.io", "roadmap", "customer success", "market insights", diff --git a/src/cli/archive.js b/src/cli/archive.js new file mode 100644 index 0000000..9b803c1 --- /dev/null +++ b/src/cli/archive.js @@ -0,0 +1,61 @@ +/** + * A class used to create or restore from a ZIP based arhive package + * @author Michael Hay + * @file archive.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 2.0.0 + */ + +// Import required modules +import zip from 'adm-zip' + +class ArchivePackage { + /** + * A class designed to enable consistent ZIP packaging for mediumroat.io archives/backups + * @constructor + * @classdesc Apply consistent operations to ZIP packages to enable users to backup and restore + * @param {String} packageName - the name of the package to either create or extract depending upoon the operation called + * @todo Look at the logic in mr_backup to determine what can be pulled into this class + */ + constructor(packageName) { + this.packageName = packageName + } + + /** + * @async + * @function createZIPArchive + * @description Create a ZIP package from a source directory + * @param {String} sourceDirectory - the full path to directory where the ZIP package will be stored + * @returns {Array} containing the status of the create operation, status message and null + */ + async createZIPArchive(sourceDirectory) { + try { + const zipPackage = new zip() + await zipPackage.addLocalFolder(sourceDirectory) + await zipPackage.writeZip(this.packageName) + return [true, `SUCCESS: Created [${this.packageName}] successfully`, null] + } catch (e) { + return [false, `ERROR: Something went wrong. [${e}]`, null] + } + } + + /** + * @async + * @function extractZIPArchive + * @description Extract objects from a ZIP package into a target directory + * @param {String} targetDirectory - the location for the ZIP package to be extracted to + * @returns {Array} containing the status of the create operation, status message and null + */ + async extractZIPArchive(targetDirectory) { + try { + const zipPackage = new zip(this.packageName) + zipPackage.extractAllTo(targetDirectory, true) + return [true, `SUCCESS: Extracted [${outputFile}] successfully`, null] + } catch (e) { + return [false, `ERROR: Something went wrong. [${e}]`, null] + } + } +} + +export default ArchivePackage \ No newline at end of file diff --git a/src/cli/common.js b/src/cli/common.js new file mode 100644 index 0000000..3e03bfe --- /dev/null +++ b/src/cli/common.js @@ -0,0 +1,109 @@ +/** + * A class for common functions for all CLIs. + * @author Michael Hay + * @file common.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.0.0 + */ + + +// Import required modules +import { Auth, Companies, Interactions, Studies } from '../api/mrServer.js' + +class serverOperations { + /** + * A class with high level operations to interact with the mediumroast.io server + * @constructor + * @classdesc Commonly needed high level operations that interact with the mediumroast.io server + * @param {Object} env - an object containing the environmental variables for instantiating access to the mediumroast.io server + * @param {String} server - a full URL to the target mediumroast.io server + */ + constructor(env, server=null) { + env.restServer ? + this.restServer = env.restServer : + this.restServer = server + env.user ? + this.user = env.user : + this.user = env.DEFAULT.user + env.secret ? + this.secret = env.secret : + this.secret = env.DEFAULT.secret + env.apiKey ? + this.apiKey = env.apiKey : + this.apiKey = env.DEFAULT.api_key + } + + /** + * @async + * @function getOwningCompany + * @description Find and return the owning company name + * @param {Object} apiController - a fully authenticated company API controller capable of talking to the mediumroast.io + * @returns {Array} the array contains [success, message, owningCompanyName], if success is true detected and return the owning company + */ + async getOwningCompany(companyCtl) { + const [success, msg, results] = await companyCtl.findByX('role','Owner') + if (success && results.length > 0) { + return [true, {status_code: 200, status_msg: 'detected owning company'}, results[0].name] + } else { + return [false, {status_code: 404, status_msg: 'owning company not found'}, null] + } + } + + /** + * @async + * @function checkServer + * @description Checks to see if the mediumroast.io sever is empty, has no objects, or not, has objects + * @returns {Array} the array contains [success, message, apiControllers], if success is true the server is empty else it isn't + */ + async checkServer() { + // Generate the credential & construct the API Controllers + const myAuth = new Auth( + this.restServer, + this.apiKey, + this.user, + this.secret, + ) + const myCredential = myAuth.login() + const interactionCtl = new Interactions(myCredential) + const companyCtl = new Companies(myCredential) + const studyCtl = new Studies(myCredential) + + // Get all objects from the server + const myStudies = await studyCtl.getAll() + const myCompanies = await companyCtl.getAll() + const myInteractions = await interactionCtl.getAll() + const [noStudies, noCompanies, noInteractions] = [myStudies[2], myCompanies[2], myInteractions[2]] + + // See if the server is empty + if (noStudies.length === 0 && noCompanies.length === 0 && noInteractions.length === 0) { + return [ + true, + {status_code: 200, status_msg: 'server empty'}, + { + restServer: this.restServer, + companyCtl: companyCtl, + interactionCtl: interactionCtl, + studyCtl: studyCtl, + credential: myCredential + } + ] + // Else the server isn't empty + } else { + return [ + false, + {status_code: 503, status_msg: 'server not empty'}, + { + restServer: this.restServer, + companyCtl: companyCtl, + interactionCtl: interactionCtl, + studyCtl: studyCtl, + credential: myCredential + } + ] + } + } +} + +export default serverOperations + diff --git a/src/cli/commonWizard.js b/src/cli/commonWizard.js index 3952173..73a3cc5 100644 --- a/src/cli/commonWizard.js +++ b/src/cli/commonWizard.js @@ -176,6 +176,18 @@ class WizardUtils { } } } + + async getRegion () { + const tmpRegion = await this.doCheckbox( + "Which region is this company associated to?", + [ + {name: 'Americas', checked: true}, + {name: 'Europe Middle East, Africa'}, + {name: 'Asia, Pacific, Japan'} + ] + ) + return tmpRegion[0] + } } diff --git a/src/cli/companyWizard.js b/src/cli/companyWizard.js index 4cf10be..e76bbfe 100755 --- a/src/cli/companyWizard.js +++ b/src/cli/companyWizard.js @@ -15,7 +15,7 @@ import ora from "ora" import mrRest from "../api/scaffold.js" import WizardUtils from "./commonWizard.js" import { Utilities } from "../helpers.js" -import CLIOutput from "../output.js" +import CLIOutput from "./output.js" class AddCompany { /** @@ -32,16 +32,17 @@ class AddCompany { * @param {Object} env - contains key items needed to interact with the mediumroast.io application * @param {Object} apiController - an object used to interact with the backend for companies * @param {Object} credential - a credential needed to talk to a RESTful service which is the company_dns in this case - * @param {Object} cli - the already constructed CLI object * @param {String} companyDNSUrl - the url to the company DNS service + * @todo replace the company_DNS url with the proper item in the config file */ - constructor(env, apiController, companyDNSUrl="http://cherokee.from-ca.com:16868"){ + constructor(env, apiController, companyDNSUrl=null){ this.env = env this.apiController = apiController this.endpoint = "/V2.0/company/merged/firmographics/" + this.env.companyDNS ? this.companyDNS = this.env.companyDNS : this.companyDNS = companyDNSUrl this.cred = { apiKey: "Not Applicable", - restServer: companyDNSUrl, + restServer: this.companyDNS, user: "Not Applicable", secret: "Not Applicable" } @@ -278,34 +279,34 @@ class AddCompany { // prototype object to do so. let companyPrototype = { name: {consoleString: "name", value:this.defaultValue}, + description: {consoleString: "description", value:this.defaultValue}, + company_type: {consoleString: "company type (e.g. Public, Private, etc.)", value:this.defaultValue}, industry: {consoleString: "industry", value:this.defaultValue}, + sic: {consoleString: "Standard Industry Code", value:this.defaultValue}, + sic_description: {consoleString: "Standard Industry Code description", value:this.defaultValue}, url: {consoleString: "website", value:this.defaultValue}, + logo_url: {consoleString: "logo url", value:this.defaultValue}, street_address: {consoleString: "street address", value:this.defaultValue}, city: {consoleString: "city", value:this.defaultValue}, state_province: {consoleString: "state or province", value:this.defaultValue}, country: {consoleString: "country", value:this.defaultValue}, + zip_postal: {consoleString: "zip or postal code", value:this.defaultValue}, + longitude: {consoleString: "longitude", value:this.defaultValue}, + latitude: {consoleString: "latitude", value:this.defaultValue}, phone: {consoleString: "phone number", value:this.defaultValue}, - description: {consoleString: "description", value:this.defaultValue}, + google_maps_url: {consoleString: "URL to locate the company on Google Maps", value:this.defaultValue}, + google_news_url: {consoleString: "URL to find news about the company on Google", value:this.defaultValue}, + google_finance_url: {consoleString: "URL to reveal financial insights on Google", value:this.defaultValue}, + google_patents_url: {consoleString: "URL to locate patent insights on Googles", value:this.defaultValue}, cik: {consoleString: "SEC Central Index Key", value:this.defaultValue}, stock_symbol: {consoleString: "stock ticker", value:this.defaultValue}, stock_exchange: {consoleString: "stock exchange", value:this.defaultValue}, recent10k_url: {consoleString: "recent form 10-K URL", value:this.defaultValue}, recent10q_url: {consoleString: "recent form 10-Q URL", value:this.defaultValue}, - zip_postal: {consoleString: "zip or postal code", value:this.defaultValue}, - longitude: {consoleString: "longitude", value:this.defaultValue}, - latitude: {consoleString: "latitude", value:this.defaultValue}, - logo_url: {consoleString: "logo url", value:this.defaultValue}, wikipedia_url: {consoleString: "wikipedia url", value:this.defaultValue}, - sic: {consoleString: "Standard Industry Code", value:this.defaultValue}, - sic_description: {consoleString: "Standard Industry Code description", value:this.defaultValue}, - company_type: {consoleString: "company type (e.g. Public, Private, etc.)", value:this.defaultValue}, firmographics_url: {consoleString: "firmographics detail URL for public companies", value:this.defaultValue}, filings_url: {consoleString: "filings URL for public companies", value:this.defaultValue}, owner_transactions: {consoleString: "URL containing share ownership reports", value:this.defaultValue}, - google_maps_url: {consoleString: "URL to locate the company on Google Maps", value:this.defaultValue}, - google_news_url: {consoleString: "URL to find news about the company on Google", value:this.defaultValue}, - google_finance_url: {consoleString: "URL to reveal financial insights on Google", value:this.defaultValue}, - google_patents_url: {consoleString: "URL to locate patent insights on Googles", value:this.defaultValue}, } // Define an empty company object @@ -334,15 +335,7 @@ class AddCompany { console.log(chalk.blue.bold('Starting location properties selections...')) // Set the region - const tmpRegion = await this.wutils.doCheckbox( - "Which region is this interaction associated to?", - [ - {name: 'Americas', checked: true}, - {name: 'Europe Middle East, Africa'}, - {name: 'Asia, Pacific, Japan'} - ] - ) - myCompany.region = tmpRegion[0] + myCompany.region = await this.wutils.getRegion() this.cutils.printLine() // Set the role diff --git a/src/env.js b/src/cli/env.js similarity index 91% rename from src/env.js rename to src/cli/env.js index d907554..fd90f24 100644 --- a/src/env.js +++ b/src/cli/env.js @@ -94,10 +94,6 @@ class Environmentals { '--find_by_x ', 'Find object by an arbitrary attribute as specified by JSON (ex \'{\"zip_postal\":\"92131\"}\')' ) - .option( - '--create ', - 'Add objects to the backend by specifying a JSON file' - ) .option( '--update ', 'Update an object from the backend by specifying the object\'s id and value to update in JSON' @@ -151,18 +147,20 @@ class Environmentals { */ getEnv(cliArgs, config) { let env = { - "restServer": null, - "apiKey": null, - "user": null, - "secret": null, - "workDir": null, - "outputDir": null, - "s3Server": null, - "s3User": null, - "s3APIKey": null, - "s3Region": null, - "s3Source": null, - "splash": null + restServer: null, + apiKey: null, + user: null, + secret: null, + workDir: null, + outputDir: null, + s3Server: null, + s3User: null, + s3APIKey: null, + s3Region: null, + s3Source: null, // TODO this is deprecated remove after testing + splash: null, + companyDNS: null + } // With the cli options as the priority set up the environment for the cli @@ -173,13 +171,13 @@ class Environmentals { // Set up additional parameters from config file env.workDir = config.get('DEFAULT', 'working_dir') - env.owningCompany = config.get('DEFAULT', 'owning_company') + env.companyDNS = config.get('DEFAULT', 'company_dns_server') env.outputDir = process.env.HOME + '/' + config.get('document_settings', 'output_dir') env.s3Server = config.get('s3_settings', 'server') env.s3User = config.get('s3_settings', 'user') env.s3Region = config.get('s3_settings', 'region') env.s3APIKey = config.get('s3_settings', 'api_key') - env.s3Source = config.get('s3_settings', 'source') + env.s3Source = config.get('s3_settings', 'source') // TODO this is deprecated remove after testing // Setup options with cli settings only env.splash = cliArgs.splash diff --git a/src/cli/filesystem.js b/src/cli/filesystem.js new file mode 100644 index 0000000..c01970a --- /dev/null +++ b/src/cli/filesystem.js @@ -0,0 +1,129 @@ +/** + * A class used to perform various file system operations + * @author Michael Hay + * @file filesystem.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 2.0.0 + */ + +// Import required modules +import * as fs from 'fs' + +class FilesystemOperators { + /** + * A class meant to make it safe and easy for file system operations + * @classdesc Enable users of the class higher level and safe operations to interact with the file system + */ + + /** + * @function saveTextFile + * @description Save textual data to a file + * @param {String} fileName - full path to the file and the file name to save to + * @param {String} text - the string content to save to a file which could be JSON, XML, TXT, etc. + * @returns {Array} containing the status of the save operation, status message and null/error + */ + saveTextFile(fileName, text) { + fs.writeFileSync(fileName, text, err => { + if (err) { + return [false, 'Did not save file [' + fileName + '] because: ' + err, null] + } + }) + return [true, 'Saved file [' + fileName + ']', null] + } + + /** + * @function readTextFile + * @description Safely read a text file of any kind + * @param {String} fileName - name of the file to read + * @returns {Array} containing the status of the read operation, status message and data read + */ + readTextFile(fileName) { + try { + const fileData = fs.readFileSync(fileName, 'utf8') + return [true, 'Read file [' + fileName + ']', fileData] + } catch (err) { + return [false, 'Unable to read file [' + fileName + '] because: ' + err, null] + } + } + + /** + * @function checkFilesystemObject + * @description Check to see if a file system object exists or not + * @param {String} name - full path to the file system object to check + * @returns {Array} containing the status of the check operation, status message and null + */ + checkFilesystemObject(name) { + if (fs.existsSync(name)) { + return [true, 'The file system object [' + name + '] was detected.', null] + } else { + return [false, 'The file system object [' + name + '] was not detected.', null] + } + } + + /** + * @function safeMakedir + * @description Resursively and safely create a directory + * @param {String} dirName - full path to the directory to create + * @returns {Array} containing the status of the mkdir operation, status message and null + */ + safeMakedir(dirName) { + try { + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }) + return [true, 'Created directory [' + dirName + ']', null] + } else { + return [true, 'Directory [' + dirName + '] exists did not create.', null] + } + } catch (err) { + return [false, 'Did not create directory [' + dirName + '] because: ' + err, null] + } + } + + /** + * @function rmDir + * @description Recursively remove a directory + * @param {String} dirName - full path to the parent directory to revmove + * @returns {Array} containing the status of the rmdir operation, status message and null + */ + rmDir(dirName) { + try { + fs.rmSync(dirName, {recursive: true}) + return [true, 'Removed directory [' + dirName + '] and all contents', null] + } catch (err) { + return [false, 'Did not remove directory [' + dirName + '] because: ' + err, null] + } + } + + /** + * @function listAllFiles + * @description List all contents of the directory + * @param {String} dirName - full path of the directory to list the contents of + * @returns {Array} containing the status of the rmdir operation, status message and either the file contents or null + */ + listAllFiles(dirName) { + try { + const myFiles = fs.readdirSync(dirName) + return [true, 'Able to access [' + dirName + '] and list all content', myFiles] + } catch (err) { + return [false, 'Unable to list contents of [' + dirName + '] because: ' + err, null] + } + } + + /** + * @function checkFilesystemObjectType + * @description Check the type of file system object + * @param {*} fileName - name of the file system object to check + * @returns {Array} containing the status of the function, status message and either the file system object type or null + */ + checkFilesystemObjectType(fileName) { + try { + const myType = fs.statSync(fileName) + return [true, 'Able to check [' + fileName + '] and its type', myType] + } catch (err) { + return [false, 'Unable to check [' + fileName + '] because: ' + err, null] + } + } +} + +export default FilesystemOperators \ No newline at end of file diff --git a/src/cli/interactionWizard.js b/src/cli/interactionWizard.js index 769d564..0f0df9a 100755 --- a/src/cli/interactionWizard.js +++ b/src/cli/interactionWizard.js @@ -6,20 +6,23 @@ * @file interactionCLIwizard.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 1.0.0 + * @version 1.2.0 */ // Import required modules import { Auth, Companies } from "../api/mrServer.js" -import inquirer from "inquirer" import chalk from 'chalk' -import ora from "ora" import path from "node:path" import crypto from "node:crypto" import WizardUtils from "./commonWizard.js" import { Utilities } from "../helpers.js" +import CLIOutput from "./output.js" +import serverOperations from "./common.js" +import s3Utilities from "./s3.js" +import FilesystemOperators from "./filesystem.js" + class AddInteraction { /** * A class which encodes the steps needed to create an interaction in the mediumroast.io application. There are two @@ -38,13 +41,8 @@ class AddInteraction { * @param {Object} credential - a credential needed to talk to a RESTful service which is the company_dns in this case * @param {Object} cli - the already constructed CLI object */ - constructor(env, apiControllers, credential, cli){ + constructor(env){ this.env = env - this.apiController = apiControllers.interaction - this.studyCtl = apiControllers.study - this.companyCtl = apiControllers.company - this.credential = credential - this.cli = cli // Splash screen elements this.name = "mediumroast.io Interaction Wizard" @@ -56,16 +54,9 @@ class AddInteraction { this.objectType = "interaction" this.wutils = new WizardUtils(this.objectType) // Utilities from common wizard this.cutils = new Utilities(this.objectType) // General package utilities - - // const myAuth = new Auth( - // env.restServer, - // env.apiKey, - // env.user, - // env.secret - // ) - // console.log(myAuth) - // const myCredential = myAuth.login() - // this.companyCtl = new Companies(myCredential) + this.output = new CLIOutput(this.env, this.objectType) + this.s3Ops = new s3Utilities(this.env) + this.fileSystem = new FilesystemOperators() } _makeChoices(myObjs) { @@ -136,19 +127,32 @@ class AddInteraction { return objs[myObjId] } - async _linkInteractionToCompany (myCompany, myInteraction) { + async _linkInteractionToCompany (myCompany, myInteraction, companyCtl) { // Hash the names const intHash = crypto.createHash('sha256', myInteraction.name).digest('hex') + let myCurrentCompany = {} + // Get the most recent copy of the company from the backend before we proceed + const currentCompany = await companyCtl.findById(myCompany.id) + if(currentCompany[0]) { + myCurrentCompany = currentCompany[2][0] + } else { + return [ + false, + {status_code: 204, status_msg: "unable to link interaction to company"}, + null + ] + } // Create and update the object link - // TODO the linking isn't working correctly - myCompany.linked_interactions[myInteraction.name] = intHash - const [success, msg, result] = await this.companyCtl.updateObj(JSON.stringify({ - id: myCompany.id, linked_interactions: myCompany.linked_interactions - })) - console.log(msg) - - if(success) { + myCurrentCompany.linked_interactions[myInteraction.name] = intHash + const linkStatus = await companyCtl.updateObj( + { + id: myCompany.id, + linked_interactions: myCurrentCompany.linked_interactions + } + ) + + if(linkStatus[0]) { return [ true, {status_code: 200, status_msg: "successfully linked interaction to company"}, @@ -180,17 +184,15 @@ class AddInteraction { let myInteraction = {} // Define the white listed properties to prompt the user for - // TODO verify the whitelist + // TODO removed interaction_type and name as they will come later on const whiteList = [ - 'name', 'street_address', 'city', - 'status_province', + 'state_province', 'zip_postal', 'country', 'region', 'phone', - 'interaction_type' ] // Study link @@ -239,7 +241,7 @@ class AddInteraction { // After assignments is successful then ask if we want a summary review or detailed review - const doSummary = await this.wutils.operationOrNot (`Would you like to do a summary review of attributes for ${prototype.name.value}?`) + const doSummary = await this.wutils.operationOrNot (`Would you like to do a summary review of attributes for your interaction(s)?`) if (await doSummary) { const tmpInteraction = await this.wutils.doManual( prototype, @@ -268,7 +270,7 @@ class AddInteraction { return myInteraction } - async doAutomatic(prototype){ + async discoverObjects(prototype){ let myCompany = {} let myStudy = {} let myInteraction = {} @@ -306,7 +308,7 @@ class AddInteraction { // Return and perform manual return [ false, - {status_code: 422, status_msg: "unable to process the automatic process for interaction creation"}, + {status_code: 422, status_msg: "unable to perform the automatic process for interaction creation"}, null ] } @@ -324,21 +326,245 @@ class AddInteraction { } - async _getFile(targetBucket, protocol='s3') { + async _uploadFile (fileName, targetBucket, protocol='s3') { + let fileBits = fileName.split('/') + const shortFilename = fileBits[fileBits.length - 1] + const myName = shortFilename.split('.')[0] + let myURL = this.env.s3Server + `/${targetBucket}/${shortFilename}` + myURL = myURL.replace('http', protocol) + const myContents = { + name: myName, + file: fileName, + url: myURL + } + const myObjectType = this.fileSystem.checkFilesystemObjectType(fileName) + if(myObjectType[2].isFile()) { + process.stdout.write(chalk.blue.bold(`\tUploading -> `)) + console.log(chalk.blue.underline(`${fileName.slice(0, 72)}...`)) + const [returnedFileName, s3Results] = await this.s3Ops.s3UploadObjs([fileName], targetBucket) + return [true, {status_code: 200, status_msg: 'successfully upladed file to storage space'},myContents] + } else { + myContents.url = this.defaultValue + return [false, {status_code: 503, status_msg: 'the source is not a file that can be uploaded'}, myContents] + } + + } + + async getFiles(targetBucket) { + // Pre-define the final object + let myFiles = [] + + // Prompt the user to see if they want to perform multi-file ingestion + const multiFile = await this.wutils.operationOrNot('Would you like to perform multi-file ingestion?') + + // Execute multi-file ingestion + if(multiFile) { + // Prompt the user for the target directory + const dirPrototype = { + dir_name: {consoleString: "target directory with path (typically, /parent_dir/company_name)", value:this.defaultValue} + } + let myDir = await this.wutils.doManual(dirPrototype) + const [success, message, result] = this.fileSystem.checkFilesystemObject(myDir.dir_name) + + // Try again if the check of the file system object fails + if (!success) { + console.log(chalk.red.bold('\t-> The file system object wasn\'t detected, perhaps the path/file name isn\'t correct? Trying again...')) + myFiles = await this.getFiles(targetBucket) // TODO test this + } + + // List all files in the directory and process them one at a time + const allFiles = this.fileSystem.listAllFiles(myDir.dir_name) + for(const myIdx in allFiles[2]) { + // Set the file name for easier readability + const fileName = allFiles[2][myIdx] + // Skip files that start with . including present and parent working directories + if(fileName.indexOf('.') === 0) { continue } // TODO check to see if this causes the problem + const myContents = await this._uploadFile(myDir.dir_name + '/' + fileName, targetBucket) + myFiles.push(myContents[2]) + } + // Execute single file ingestion + } else { + // Prompt the user for the target file + const filePrototype = { + file_name: {consoleString: "target file with path (typically, /parent_dir/sub_dir/file_name.ext)", + value:this.defaultValue} + } + let myFile = await this.wutils.doManual(filePrototype) + const [success, message, result] = this.fileSystem.checkFilesystemObject(myFile.file_name) + + // Try again if the check of the file system object fails + if (!success) { + console.log(chalk.red.bold('\t-> The file system object wasn\'t detected, perhaps the path/file name isn\'t correct? Trying again...')) + myFiles = await this.getFiles(targetBucket) // TODO test this + } + + // Upload the file + const myContents = await this._uploadFile(myFile.fileName, targetBucket) + myFiles.push(myContents[2]) + } + + // An end separator + this.output.printLine() + + // Return the result of uploaded files + return myFiles + } + + async createInteraction(myInteraction, targetBucket) { + let myFiles = [] + + // Perform basic definitional work + myCompany = await this.wutils.doManual(interactionPrototype) + this.output.printLine() + + console.log(chalk.blue.bold('Setting location properties...')) + // Set the region + myInteraction.region = this.wutils.getRegion() + + // Set lat, long and address + const myLocation = await this.wutils.getLatLong(myInteraction) // Based upon entered data discover the location(s) + myInteraction.latitude = myLocation.latitude // Set to discovered value + myInteraction.longitude = myLocation.longitude // Set to discovered value + myInteraction.street_address = myLocation.formattedAddress // Set to discovered value + this.output.printLine() + + console.log(chalk.blue.bold('Preparing to ingest interaction file.')) const filePrototype = { - file_name: {consoleString: "file name with path (e.g., /dir/sub_dir/file_name)", value:this.defaultValue} + file_name: { + consoleString: "file name with path (typically, /parent_dir/sub_dir/file_name.ext)", + value: this.defaultValue + } } let myFile = await this.wutils.doManual(filePrototype) - const [success, message, result] = this.cutils.checkFilesystemObject(myFile.file_name) + const [success, message, result] = this.fileSystem.checkFilesystemObject(myFile.file_name) // Try again if we don't actually see the file exists if(!success) { - console.log(chalk.red.bold('\t-> The file wasn\'t detected, perhaps the path/file name isn\'t correct? Trying again...')) - myFile = await this._getFile() // TODO this won't work... + console.log(chalk.red.bold('\t-> The file system object wasn\'t detected, perhaps the path/file name isn\'t correct? Trying again...')) + myFiles = await this.createInteraction(myInteraction) } - console.log(chalk.blue.bold(`Uploading [${myFile.file_name}] to S3...`)) - const [fileName, uploadResults] = await this.cutils.s3UploadObjs([myFile.file_name], this.env, targetBucket) - let myUrl = this.env.s3Server + `/${targetBucket}/${fileName}` - return [myUrl.replace('http', protocol), fileName] + const myContents = await this._uploadFile(myFile.file_name, targetBucket) + myFiles.push(myContents[2]) + return myFiles + } + + async _chooseInteractionType () { + let interactionType = this.defaultValue + const tmpType = await this.wutils.doCheckbox( + "What kind of interaction is this?", + [ + {name: 'General Notes'}, + {name: 'Frequently Asked Questions'}, + {name: 'White Paper'}, + {name: 'Case Study'}, + {name: 'US SEC Filing'}, + {name: 'Patent'}, + {name: 'Press Release'}, + {name: 'Announcement'}, + {name: 'Blog Post'}, + {name: 'Product Manual'}, + {name: 'Transcript'}, + {name: 'About the company'}, + {name: 'Research Paper'}, + {name: 'Other'}, + ] + ) + if(tmpType[0] === 'Other') { + const typePrototype = { + type_name: { + consoleString: "type?", + value: interactionType + } + } + interactionType = await this.wutils.doManual(typePrototype) + } else { + interactionType = tmpType[0] + } + return interactionType + } + + async _mergeResults(controller, interaction, files, company, companyCtl) { + let interactionResults = {} + + for (const myFile in files) { + process.stdout.write(chalk.blue.bold(`\tCreating interaction -> `)) + console.log(chalk.blue.underline(`${files[myFile].name.slice(0, 72)}...`)) + let myInteraction = interaction + + // Set the interaction_type property + myInteraction.interaction_type = await this._chooseInteractionType() + + // Name + myInteraction.name = files[myFile].name + // URL + myInteraction.url = files[myFile].url + // Status + myInteraction.status = 0 + // Abstract + myInteraction.abstract = this.defaultValue + // Description + myInteraction.description = this.defaultValue + // Public + myInteraction.public = false + // Topics + myInteraction.topics = {} + // Groups + myInteraction.groups = `${this.env.user}:${this.env.user}` + // Current time + const myDate = new Date() + myInteraction.creation_date = myDate.toISOString() + myInteraction.modification_date = myDate.toISOString() + myInteraction.date_time = myDate.toISOString() + // Creator and Owner ID + myInteraction.creator_id = 1 // we will need to change this to be determined from the environment + myInteraction.owner_id = 1 // we will need to change this to be determined from the environment + // File metadata + myInteraction.content_type = this.defaultValue + myInteraction.file_size = this.defaultValue + myInteraction.reading_time = this.defaultValue + myInteraction.word_count = this.defaultValue + myInteraction.page_count = this.defaultValue + console.log(chalk.blue(`\t\tSaving interaction...`)) + const [createSuccess, createMessage, createResults] = await controller.createObj(myInteraction) + let linkResults = [] + if (createSuccess) { + // TODO revist the linking of studies and companies, these are placeholders for now + console.log(chalk.blue(`\t\tLinking interaction to company -> ${company.name}`)) + linkResults = await this._linkInteractionToCompany(company, myInteraction, companyCtl) + // const [success, msg, intLinkStudy] = this._linkInteractionToStudy(myStudy, interaction) + } + this.output.printLine() + const linkSuccess = linkResults[0] + if(createSuccess && linkSuccess) { + interactionResults[myInteraction.name] = [ + createSuccess, + {status_code: 200, status_msg: `successfully created and linked ${myInteraction.name}`}, + null + ] + } else if(createSuccess && !linkSuccess) { + interactionResults[myInteraction.name] = [ + createSuccess, + {status_code: 503, status_msg: `successfully created but could not link ${myInteraction.name}`}, + null + ] + } else if(!createSuccess && linkSuccess) { + interactionResults[myInteraction.name] = [ + createSuccess, + {status_code: 503, status_msg: `successfully linked but could not create ${myInteraction.name}`}, + null + ] + } else { + interactionResults[myInteraction.name] = [ + createSuccess, + {status_code: 404, status_msg: `unable to create or link ${myInteraction.name}`}, + null + ] + } + } + return [ + true, + {status_code: 200, status_msg: `performed create and link operations on ${interactionResults.length}`}, + interactionResults + ] } /** @@ -349,7 +575,7 @@ class AddInteraction { async wizard() { // Unless we suppress this print out the splash screen. if (this.env.splash) { - this.cli.splashScreen( + this.output.splashScreen( this.name, this.version, this.description @@ -363,6 +589,7 @@ class AddInteraction { // prototype object to do so. let interactionPrototype = { name: {consoleString: "name", value:this.defaultValue}, + // TODO this has to come out interaction_type: {consoleString: "interaction type (e.g. whitepaper, interview, etc.)", value:this.defaultValue}, street_address: {consoleString: "street address (i.e., where interaction takes place)", value:this.defaultValue}, city: {consoleString: "city (i.e., where interaction takes place)", value:this.defaultValue}, @@ -377,10 +604,11 @@ class AddInteraction { contact_twitter: {consoleString: "contact\'s Twitter handle", value:this.defaultValue}, } - // Define an empty interaction object + // Define an empty objects let myInteraction = {} let myCompany = {} let myStudy = {} + let myFiles = [] // Choose if we want to run the setup or not, and it not exit the program const doSetup = await this.wutils.operationOrNot('It appears you\'d like to create a new interaction, right?') @@ -389,123 +617,64 @@ class AddInteraction { process.exit() } - // Choose if we want to run the setup or not, and it not exit the program - console.log(chalk.blue.bold('Prompting for interaction file...')) - // NOTE: Eventually we will have an approach were we will either add a file or merely link to a URL where the file - // resides. - // const doFile = await this.wutils.operationOrNot('Is there an file for the interaction you\'d like to include?') - // if (doFile) { - // const myUrl = await this._getFile() - // myInteraction.url = myUrl - // } - // TODO set the bucket to a target... - // TODO add a property in the config file to set the owner org, we map this to a bucket in minio this will clarify which bucket we should use - const [myUrl, fileName] = await this._getFile(this.env.owningCompany) - console.log('Filename:', fileName) - - interactionPrototype.name.value = fileName.split('.')[0] // Define the name from the file name in the default value - this.cutils.printLine() - - // Choose if we want manual or automatic - const automatic = await this.wutils.operationOrNot('Would like to proceed with automatic interaction creation?') - - // Perform automated processing - let [myObjs, autoSuccess, autoMsg] = [{}, null, {}] - if (automatic) { - // Perform auto setup - console.log(chalk.blue.bold('Starting automatic interaction creation...')) - const [success, msg, objs] = await this.doAutomatic(interactionPrototype) - myObjs = objs - autoSuccess = success - autoMsg = msg - myInteraction = myObjs.interaction - myCompany = myObjs.company - myStudy = myObjs.study + // TODO the below can be moved to commonWizard + // Checking to see if the server is ready for adding interactions + process.stdout.write(chalk.blue.bold('\tPerforming checks to see if the server is ready to ingest interactions. ')) + const serverChecks = new serverOperations(this.env) + const serverReady = await serverChecks.checkServer() + if(!serverReady[0]) { // NOTE: We are looking for a false return here because it means there are objects which we need to proceeed + console.log(chalk.green.bold('[Ready]')) + } else { + console.log(chalk.red.bold('[No objects detected, exiting]')) + process.exit(-1) } - - // Perform manual processing if the user selected that or if auto fails - if (!automatic && !autoSuccess) { - // Perform manual setup - console.log(chalk.blue.bold('Starting manual interaction creation...')) - myCompany = await this.wutils.doManual(interactionPrototype) - this.cutils.printLine() - console.log(chalk.blue.bold('Starting location properties selections...')) - // Set the region - const tmpRegion = await this.wutils.doCheckbox( - "Which region is this interaction associated to?", - [ - {name: 'Americas', checked: true}, - {name: 'Europe Middle East, Africa'}, - {name: 'Asia, Pacific, Japan'} - ] - ) - myInteraction.region = tmpRegion[0] - - // Set lat, long and address - const myLocation = await this.wutils.getLatLong(myInteraction) // Based upon entered data discover the location(s) - myInteraction.latitude = myLocation.latitude // Set to discovered value - myInteraction.longitude = myLocation.longitude // Set to discovered value - myInteraction.street_address = myLocation.formattedAddress // Set to discovered value - this.cutils.printLine() + + // Assign the controllers based upon the available server + const companyCtl = serverReady[2].companyCtl + const interactionCtl = serverReady[2].interactionCtl + const studyCtl = serverReady[2].studyCtl + + // Detect owning company and generate the target bucket name + let owningCompanyName = null + let targetBucket = null // We'll use this for storing interactions + process.stdout.write(chalk.blue.bold('\tDetecting the owning company for this mediumroast.io server. ')) + const owningCompany = await serverChecks.getOwningCompany(companyCtl) + if(owningCompany[0]){ + owningCompanyName = owningCompany[2] + targetBucket = this.s3Ops.generateBucketName(owningCompanyName) + console.log(chalk.green.bold(`[${owningCompanyName}]`)) + this.output.printLine() + } else { + console.log(chalk.red.bold('[No owning company detected, exiting]')) + process.exit(-1) } - this.cutils.printLine() - - console.log(chalk.blue.bold('Setting special attributes to known values...')) - // URL - myInteraction.url = myUrl // Define the URL - // Status - myInteraction.status = 0 - // Abstract - myInteraction.abstract = this.defaultValue - // Description - myInteraction.description = this.defaultValue - // Public - myInteraction.public = false - // Topics - myInteraction.topics = {} - // Groups - myInteraction.groups = `${this.env.user}:${this.env.user}` - // Current time - const myDate = new Date() - myInteraction.creation_date = myDate.toISOString() - myInteraction.modification_date = myDate.toISOString() - myInteraction.date_time = myDate.toISOString() - // Creator and Owner ID - myInteraction.creator_id = 1 // we will need to change this to be determined from the environment - myInteraction.owner_id = 1 // we will need to change this to be determined from the environment - // File metadata - myInteraction.content_type = this.defaultValue - myInteraction.file_size = this.defaultValue - myInteraction.reading_time = this.defaultValue - myInteraction.word_count = this.defaultValue - myInteraction.page_count = this.defaultValue - this.cutils.printLine() + // Perform automated Company and Study object discovery + console.log(chalk.blue.bold('Discovering relevant mediumroast.io objects.')) + const [autoSuccess, autoMsg, myObjs] = await this.discoverObjects(interactionPrototype) + if(autoSuccess) { + // Assign results if automatic discovery was successful + myInteraction = myObjs.interaction + myCompany = myObjs.company + myStudy = myObjs.study + // Get the individual files which will be transformed into interactions + myFiles = await this.getFiles(targetBucket) - console.log(chalk.blue.bold(`Saving interaction ${myInteraction.name} to mediumroast.io...`)) - const [createSuccess, createMessage, createResults] = await this.apiController.createObj(myInteraction) - if (createSuccess) { - // TODO revist the linking of studies and companies, these are placeholders for now - // NOTE We could incrementally link things??? - const [success, msg, intLinkCompany] = await this._linkInteractionToCompany(myCompany, myInteraction) - // const [success, msg, intLinkStudy] = this._linkInteractionToStudy(myStudy, prototype) - return [ - true, - {status_code: 200, status_msg: "successfully created and linked interaction"}, - null - ] + // Fallback to manual setup for creating the interaction since discovery failed } else { - return [ - false, - {status_code: 500, status_msg: "unable to create or link interaction"}, - null - ] + console.log(chalk.orange.bold('Object discovery failed, falling back to manual processing.')) + const myObjs = await this.createInteraction(myInteraction) + myInteraction = myObjs.interaction + myCompany = myObjs.company + myStudy = myObjs.study + myFiles = myObjs.files } - - // return myInteraction + + // Merge the file names with the interaction prototype to create the interactions + return await this._mergeResults(interactionCtl, myInteraction, myFiles, myCompany, companyCtl) } } -export { AddInteraction } \ No newline at end of file +export default AddInteraction \ No newline at end of file diff --git a/src/output.js b/src/cli/output.js similarity index 93% rename from src/output.js rename to src/cli/output.js index 81c0d14..ec7b9d9 100644 --- a/src/output.js +++ b/src/cli/output.js @@ -4,7 +4,7 @@ * @file output.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 2.1.0 + * @version 2.1.1 */ // Import required modules @@ -12,7 +12,8 @@ import Table from 'cli-table' import Parser from 'json2csv' import * as XLSX from 'xlsx' import logo from 'asciiart-logo' -import { Utilities } from './helpers.js' +// import { Utilities } from '../helpers.js' // TODO Delete this as it is no longer needed +import FilesystemOperators from './filesystem.js' class CLIOutput { /** @@ -25,7 +26,7 @@ class CLIOutput { constructor(env, objectType) { this.env = env this.objectType = objectType - this.utils = new Utilities(objectType) + this.fileSystem = new FilesystemOperators() } /** @@ -52,7 +53,6 @@ class CLIOutput { head: ['Id', 'Name', 'Description'], colWidths: [5, 40, 90] }) - for (const myObj in objects) { table.push([ objects[myObj].id, @@ -68,7 +68,7 @@ class CLIOutput { const fileName = 'Mr_' + this.objectType + '.csv' const myFile = this.env.outputDir + '/' + fileName const csv = Parser.parse(objects) - this.utils.saveTextFile(myFile, csv) + this.fileSystem.saveTextFile(myFile, csv) } // TODO add error checking via try catch @@ -100,6 +100,7 @@ class CLIOutput { textColor: 'orange', } // Print out the splash screen + console.clear() console.log( logo(logoConfig) .emptyLine() diff --git a/src/s3.js b/src/cli/s3.js similarity index 79% rename from src/s3.js rename to src/cli/s3.js index f91e6a0..2d53f28 100644 --- a/src/s3.js +++ b/src/cli/s3.js @@ -17,17 +17,28 @@ class s3Utilities { * downloading from S3, reading files, creating ZIP archives, etc. * @constructor * @classdesc Construct the S3 controller needed to perform various actions - * @param {Object} env - An object containing all needed environmental variables + * @param {Object} env - An object containing all needed environmental variables for setting up the S3 controller */ constructor(env) { - this.env = env + env.s3Server ? + this.s3Server = env.s3Server : + this.s3Server = env.server + env.s3User ? + this.s3User = env.s3User : + this.s3User = env.user + env.s3APIKey ? + this.s3APIKey = env.s3APIKey : + this.s3APIKey = env.api_key + env.s3Region ? + this.s3Region = env.s3Region : + this.s3Region = env.region this.s3Controller = new AWS.S3({ - accessKeyId: this.env.user , - secretAccessKey: this.env.api_key, - endpoint: this.env.server , + accessKeyId: this.s3User , + secretAccessKey: this.s3APIKey, + endpoint: this.s3Server , s3ForcePathStyle: true, // needed with minio? signatureVersion: 'v4', - region: this.env.region // S3 won't work without the region setting + region: this.s3Region // S3 won't work without the region setting }) } @@ -38,11 +49,11 @@ class s3Utilities { * @param {String} targetDirectory - the target location for downloading the objects to * @todo this.env.s3Source is incorrect meaning it will fail for now, add srcBucket as argument */ - async s3DownloadObjs (interactions, targetDirectory) { + async s3DownloadObjs (interactions, targetDirectory, sourceBucket) { for (const interaction in interactions) { const objWithPath = interactions[interaction].url.split('://').pop() const myObj = objWithPath.split('/').pop() - const myParams = {Bucket: this.env.s3Source, Key: myObj} + const myParams = {Bucket: sourceBucket, Key: myObj} const myFile = fs.createWriteStream(targetDirectory + '/' + myObj) const s3Get = await this.s3Controller.getObject(myParams).promise() myFile.write(s3Get.Body) @@ -103,6 +114,16 @@ class s3Utilities { } } + + /** + * + * @param {String} objectName + * @returns + */ + generateBucketName(objectName) { + let bucketName = objectName.replace(/[^a-z0-9]/gi,'') + return bucketName.toLowerCase() + } } export default s3Utilities \ No newline at end of file