diff --git a/cli/company.js b/cli/company.js index 83c7b3f..aebb10a 100755 --- a/cli/company.js +++ b/cli/company.js @@ -14,7 +14,7 @@ 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 AddCompany from '../src/cli/companyWizard.js' // Globals const objectType = 'company' diff --git a/cli/setup.js b/cli/setup.js index 00f1e5c..369d4db 100755 --- a/cli/setup.js +++ b/cli/setup.js @@ -6,22 +6,35 @@ * @file mr_setup.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 - * @version 1.0.0 + * @version 2.0.0 */ // Import required modules import { Utilities } from '../src/helpers.js' -import ConfigParser from 'configparser' +import { Auth, Companies, Interactions, Studies } from '../src/api/mrServer.js' +import CLIOutput from '../src/output.js' +import WizardUtils from '../src/cli/commonWizard.js' +import AddCompany from '../src/cli/companyWizard.js' +import s3Utilities from '../src/s3.js' + import program from 'commander' -import inquirer from 'inquirer' -import logo from 'asciiart-logo' import chalk from 'chalk' +import ConfigParser from 'configparser' +import crypto from "node:crypto" + +/* + ----------------------------------------------------------------------- + + FUNCTIONS - Key functions needed for MAIN + + ----------------------------------------------------------------------- +*/ function parseCLIArgs() { // Define commandline options program .name("mr_setup") - .version('1.0.0') + .version('2.0.0') .description('A utility for setting up the mediumroast.io CLI.') program @@ -41,19 +54,22 @@ function parseCLIArgs() { function getEnv () { return { DEFAULT: { - // TODO Create choices for the rest_server so the user doesn't have to figure this out - rest_server: ["http://cherokee.from-ca.com:16767", "http://cherokee.from-ca.com:26767"], + rest_servers: ["http://cherokee.from-ca.com:16767", "http://cherokee.from-ca.com:26767"], user: "rflores", // For now we're not going to prompt for this it is a placeholder secret: "password", // For now we're not going to prompt for this it is a placeholder api_key: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJmbG9yZXMiLCJjb21wYW55IjoieCIsImlhdCI6MTY1NTAwNDM2NH0.znocDyjS4VSS9tu_ND-pUKw76yNgseUUHYpJ1Tq87do", - working_dir: "/tmp" + working_dir: "/tmp", + company_dns_servers: { + "http://cherokee.from-ca.com:16767": "http://cherokee.from-ca.com:16868", + "http://cherokee.from-ca.com:26767": "http://cherokee.from-ca.com:26868" + } }, s3_settings: { user: "medium_roast_io", api_key: "b7d1ac5ec5c2193a7d6dd61e7a8a76451885da5bd754b2b776632afd413d53e7", server: "http://cherokee.from-ca.com:9000", region: "leo-dc", - source: "openvault" + source: "Unknown" }, document_settings: { font_type: "Avenir Next", @@ -67,134 +83,136 @@ function getEnv () { } } -// TODO replace this with the splash screen in the output module -// If not suppressed print the splash screen to the console -function splashScreen (simple=false) { - const logoConfig = { - name: "mediumroast.io CLI Setup", - // font: 'Speed', - lineChars: 10, - padding: 3, - margin: 3, - borderColor: 'bold-gray', - logoColor: 'bold-orange', - textColor: 'orange', - } - // Print out the splash screen - console.log( - logo(logoConfig) - .emptyLine() - .right('version 1.0.0') - .emptyLine() - .center( - "Prompt based setup for mediumroast.io command line interface." - ) - .render() +async function _checkServer(server, env) { + // Generate the credential & construct the API Controllers + const myAuth = new Auth( + server, + env.DEFAULT.api_key, + env.DEFAULT.user, + env.DEFAULT.secret, ) -} + const myCredential = myAuth.login() + const interactionCtl = new Interactions(myCredential) + const companyCtl = new Companies(myCredential) + const studyCtl = new Studies(myCredential) + + // Check to see if the server is empty + 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]] -// TODO Replace with commonWizard functions -// Check to see if we are going to need to perform a setup operation or not. -async function checkSetup(fileName) { - const utils = new Utilities('setup') - const [exists, message, result] = utils.checkFilesystemObject(fileName) - if(exists) { - await inquirer - .prompt([ - { - name: "run_setup", - type: "confirm", - message: "Hi, I've detected an existing configuration for mediumroast.io. Should I proceed?" - } - ]) - // If we don't want to perform the setup then exit - .then((answer) => { - if (!answer.run_setup) { - console.log(chalk.red.bold('\t-> Ok exiting the CLI setup.')) - process.exit(0) - } - } - ) + // See if the server is empty + if (noStudies.length === 0 && noCompanies.length === 0 && noInteractions.length === 0) { + return [true, {status_code: 200, status_msg: 'server is ready for use'}, {restServer: server, apiController: companyCtl, credential: myCredential}] } else { - console.log(chalk.blueBright.bold('Starting the configuration process for the mediumroast.io CLI.')) + return [false, {status_code: 503, status_msg: 'server not ready for use'}, null] } - // We will always return true, because if the user decides to not proceed checkSetup exits - return true } -// TODO replace with commonWizard functions -// Prompt user to change any settings or keep the default -async function doSettings(env, isDefault=false) { - // TODO if either password or user for now we can suppress and immediately set it - let myAnswers = {} - for (const setting in env) { - // Skip user and secret if this is the DEFAULT section - // TODO eventually replace this with a proper user and password setting - if(isDefault && (setting === 'user' || setting === 'secret')) { - myAnswers[setting] = env[setting] +async function discoverServers (servers, env) { + let candidateServers = {} + const serverPrefix = 'mediumroast.io server - ' + let idx = 1 + + // Check to see if the servers are available + for (const myServer in servers) { + const serverResponse = await _checkServer(servers[myServer], env) + if (serverResponse[0]) { + candidateServers[serverPrefix + String(idx)] = serverResponse[2] + idx += 1 + } else { continue } - await inquirer - .prompt([ - { - name: setting, - type: 'input', - message: 'Set ' + setting + '?', - default() { - return env[setting] - } - } - ]) - .then(async (answer) => { - myAnswers[setting] = await answer[setting] - }) } - return myAnswers + + // Determine if we have any servers and if so return the candidates otherwise return false, etc. + const availableServers = Object.keys(candidateServers).length + if (availableServers > 0) { + return [true, {status_code: 200, status_msg: 'one or more servers is available'}, candidateServers] + } else { + return [false, {status_code: 503, status_msg: 'no servers are ready please try again'}, null] + } } -// Perform environmental variable settings for all sections -async function checkSection(env, sectionType) { - const line = '-'.repeat(process.stdout.columns) - console.log(line) - let myAnswers = {} - await inquirer - .prompt([ - { - name: "run", - type: "confirm", - message: "Setup section " + sectionType + " for the CLI?" - } - ]) - // If we don't want to perform the setup then move along and return the defaults - .then(async (answer) => { - if (!answer.run) { - console.log( - chalk.blue.bold( - '\t-> Ok you don\'t want to change the ' + - sectionType + ' settings. Populating defaults.' - ) - ) - myAnswers = env[sectionType] - } else { - sectionType === 'DEFAULT' ? - myAnswers = await doSettings(env[sectionType], true): - myAnswers = await doSettings(env[sectionType], false) - - } - } - ) - // At this point we've decided to proceed - return myAnswers - +// Check to see if the directory for the configuration exists, and +// if not create it. Also return the full path to the configuration +// file. +function checkConfigDir(configDir='/.mediumroast', configFile='config.ini') { + utils.safeMakedir(process.env.HOME + configDir) + return process.env.HOME + configDir + '/' + configFile +} + +// Save the configuration file +function writeConfigFile(myConfig, configFile) { + // Write the config file + const configurator = new ConfigParser() + for(const section in myConfig){ + configurator.addSection(section) + for(const setting in myConfig[section]){ + configurator.set(section, setting, myConfig[section][setting]) + } + } + // This won't return anything so we'll need to see if we can find another way to determine success/failure + configurator.write(configFile) +} + +// Verify the configuration was written +function verifyConfiguration(myConfig, configFile) { + const configurator = new ConfigParser() + // Read in the config file and check to see if things are ok by confirming the rest_server value matches + configurator.read(configFile) + const newRestServer = configurator.get('DEFAULT', 'rest_server') + let success = false + if(newRestServer === myConfig.DEFAULT.rest_server) { success = true } + return success } +// Generate a consistent bucket name with only alphanumeric characters, +// no spaces, and only lowercase text. +function _generateBucketName(companyName) { + let tmpName = companyName.replace(/[^a-z0-9]/gi,'') + return tmpName.toLowerCase() +} + +/* + ----------------------------------------------------------------------- + + MAIN - Steps below represent the main function of the program + + ----------------------------------------------------------------------- +*/ + // Parse the commandline arguements const myArgs = parseCLIArgs() +// Get the key settings to create the configuration file +let myEnv = getEnv() + +// Construct needed classes +const cliOutput = new CLIOutput(myEnv) +const wizardUtils = new WizardUtils('all') +const utils = new Utilities("all") + // Unless we suppress this print out the splash screen. if (myArgs.splash === 'yes') { - splashScreen() + console.clear() // Attempt to clear the screen + cliOutput.splashScreen( + "mediumroast.io Setup Wizard", + "version 2.0.0", + "CLI prompt based setup and registration for the mediumroast.io application." + ) +} + +// Check for and create the directory process.env.HOME/.mediumroast +const configFile = checkConfigDir() + +// Are we going to proceed or not? +const doSetup = await wizardUtils.operationOrNot('You\'d like to setup the mediumroast.io CLI, right?') +if (!doSetup) { + console.log(chalk.red.bold('\t-> Ok exiting CLI setup.')) + process.exit() } // Define the basic structure of the new object to store to the config file @@ -204,52 +222,102 @@ let myConfig = { document_settings: null } -// Get the key settings to create the configuration file -let myEnv = getEnv() - -// Check for and create the directory process.env.HOME/.mediumroast -const utils = new Utilities(null) -utils.safeMakedir(process.env.HOME + '/.mediumroast') -const fileName = process.env.HOME + '/.mediumroast/config.ini' +// Check to see which servers are available for use +console.log(chalk.blue.bold('Discovering available mediumroast.io servers...')) +let serverChoice = null +const serverSuccess = await discoverServers(myEnv.DEFAULT.rest_servers, myEnv) +const serverOptions = [] -// Are we going to proceed or not? -const doSetup = await checkSetup(fileName) +if (serverSuccess[0]) { + for (const candidate in serverSuccess[2]) { + serverOptions.push({name: candidate}) + } + serverChoice = await wizardUtils.doCheckbox ( + 'Please pick from one of the following servers.', + serverOptions + ) +} else { + console.log(chalk.red.bold('ERROR: No servers are available at the present time, please try again later.')) + process.exit(-1) +} +myEnv.DEFAULT['rest_server'] = serverSuccess[2][serverChoice].restServer +myEnv.DEFAULT['company_dns_server'] = myEnv.DEFAULT['company_dns_servers'][myEnv.DEFAULT['rest_server']] +const companyCtl = serverSuccess[2][serverChoice].apiController +const credential = serverSuccess[2][serverChoice].credential +delete myEnv.DEFAULT.rest_servers +delete myEnv.DEFAULT.company_dns_servers +cliOutput.printLine() -// Determine if we should setup the defaults, and if so process them -myConfig.DEFAULT = await checkSection(myEnv, 'DEFAULT') -// Determine if we should setup the s3_settings, and if so process them -myConfig.s3_settings = await checkSection(myEnv, 's3_settings') +// Create the first "owning company" for the initial user +console.log(chalk.blue.bold('Creating owning company...')) +myEnv.splash = false +const cWizard = new AddCompany( + myEnv, + companyCtl, + myEnv.DEFAULT['company_dns_server'] +) +const companyResp = await cWizard.wizard(true) +const myCompany = companyResp[1].data +// Create an S3 bucket derived from the company name, and the steps for creating the +// bucket name are in _genereateBucketName(). +const bucketName = _generateBucketName(myCompany.name) +const myS3 = new s3Utilities(myEnv.s3_settings) +const s3Resp = await myS3.s3CreateBucket(bucketName) +if(s3Resp) { + console.log(chalk.blue.bold(`Added interaction storage space for ${myCompany.name}.`)) +} else { + console.log(chalk.blue.red(`Unable to add interaction storage space for ${myCompany.name}.`)) +} +cliOutput.printLine() -// Determine if we should setup the s3_settings, and if so process them -myConfig.document_settings = await checkSection(myEnv, 'document_settings') +// Create a default study for interactions to use +console.log(chalk.blue.bold(`Adding default study to the backend...`)) +const studyCtl = new Studies(credential) +const myStudy = { + name: 'Default Study', + description: 'A placeholder study to ensure that interactions are able to have something to link to', + public: false, + groups: 'default:default', + document: {} +} +const studyResp = await studyCtl.createObj(myStudy) +cliOutput.printLine() -// TODOs -// Create the first "owning company" which is associated to the user by calling the cli wizard for companies -// We should create a bucket in the object store based upon company -// Make user we add the owning_company property to the config file +// Persist and verify the config file // Write the config file -const configurator = new ConfigParser() -for(const section in myConfig){ - configurator.addSection(section) - for(const setting in myConfig[section]){ - configurator.set(section, setting, myConfig[section][setting]) - } -} -// This won't return anything so we'll need to see if we can find another way to determine success/failure -configurator.write(fileName) +myConfig.DEFAULT = myEnv.DEFAULT +myConfig.s3_settings = myEnv.s3_settings +myConfig.document_settings = myEnv.document_settings +console.log(chalk.blue.bold('Writing configuration file [' + configFile + '].')) +writeConfigFile(myConfig, configFile) + +// Verify the config file +console.log(chalk.blue.bold('Verifying existence and contents of configuration file [' + configFile + '].')) +const success = verifyConfiguration(myConfig, configFile) +success ? + console.log(chalk.blue.bold('SUCCESS: Verified configuration file [' + configFile + '].')) : + console.log(chalk.red.bold('ERROR: Unable to verify configuration file [' + configFile + '].')) +cliOutput.printLine() -// Read in the config file and check to see if things are ok by confirming the rest_server value matches -configurator.read(fileName) -const newRestServer = configurator.get('DEFAULT', 'rest_server') -let success = false -if(newRestServer === myConfig.DEFAULT.rest_server) { success = true } +// List all create objects to the console +console.log(chalk.blue.bold(`Fetching and listing all created objects...`)) +console.log(chalk.blue.bold(`Default Study:`)) +const myStudies = await studyCtl.getAll() +cliOutput.outputCLI(myStudies[2]) +cliOutput.printLine() +console.log(chalk.blue.bold(`Registered Company:`)) +const myCompanies = await companyCtl.getAll() +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(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.') +cliOutput.printLine() -const line = '-'.repeat(process.stdout.columns) - console.log(line) -success ? - console.log(chalk.blue.bold('SUCCESS: Verified configuration file [' + fileName + '] was written.')) : - console.log(chalk.red.bold('ERROR: Unable to verify configuration file [' + fileName + '] was written.')) \ No newline at end of file diff --git a/package.json b/package.json index c07b940..842afa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.3.7", + "version": "0.3.8", "description": "A Javascript SDK to interact with mediumroast.io including command line interfaces.", "main": "src/api/mrServer.js", "scripts": { diff --git a/src/cli/commonWizard.js b/src/cli/commonWizard.js index 669784e..3952173 100644 --- a/src/cli/commonWizard.js +++ b/src/cli/commonWizard.js @@ -72,15 +72,15 @@ class WizardUtils { continue } } - // TODO check to see if the length of setting is more than the console line lenght and if so truncate + const myMessage = `What\'s the ${this.objectType}\'s ` + prototype[setting].consoleString + '?' await inquirer .prompt([ { name: setting, type: 'input', - message: `What\'s the ${this.objectType}\'s ` + prototype[setting].consoleString + '?', + message: myMessage, default() { - return prototype[setting].value + return prototype[setting].value } } ]) @@ -179,4 +179,4 @@ class WizardUtils { } -export { WizardUtils } \ No newline at end of file +export default WizardUtils \ No newline at end of file diff --git a/src/cli/companyWizard.js b/src/cli/companyWizard.js index 4c51458..4cf10be 100755 --- a/src/cli/companyWizard.js +++ b/src/cli/companyWizard.js @@ -13,8 +13,9 @@ import inquirer from "inquirer" import chalk from 'chalk' import ora from "ora" import mrRest from "../api/scaffold.js" -import { WizardUtils } from "./commonWizard.js" +import WizardUtils from "./commonWizard.js" import { Utilities } from "../helpers.js" +import CLIOutput from "../output.js" class AddCompany { /** @@ -34,14 +35,17 @@ class AddCompany { * @param {Object} cli - the already constructed CLI object * @param {String} companyDNSUrl - the url to the company DNS service */ - constructor(env, apiController, credential, cli, companyDNSUrl="http://cherokee.from-ca.com:16868"){ + constructor(env, apiController, companyDNSUrl="http://cherokee.from-ca.com:16868"){ this.env = env this.apiController = apiController - this.credential = credential this.endpoint = "/V2.0/company/merged/firmographics/" - this.credential.restServer = companyDNSUrl - this.rest = new mrRest(this.credential) - this.cli = cli + this.cred = { + apiKey: "Not Applicable", + restServer: companyDNSUrl, + user: "Not Applicable", + secret: "Not Applicable" + } + this.rest = new mrRest(this.cred) // Splash screen elements this.name = "mediumroast.io Company Wizard" @@ -53,6 +57,7 @@ class AddCompany { this.objectType = "Companies" this.wutils = new WizardUtils(this.objectType) // Utilities from common wizard this.cutils = new Utilities(this.objectType) // General package utilities + this.output = new CLIOutput(this.env, this.objectType) } @@ -230,8 +235,7 @@ class AddCompany { if (doSummary) { myCompanyObj = await this.wutils.doManual( prototype, - [ - 'description', + [ 'name', 'phone', 'website', @@ -257,10 +261,10 @@ class AddCompany { * @description Invoke the text based wizard process to add a company to the mediumroast.io application * @returns {List} - a list containing the result of the interaction with the mediumroast.io backend */ - async wizard(isOwner=null) { + async wizard(isOwner=false) { // 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 @@ -342,9 +346,8 @@ class AddCompany { this.cutils.printLine() // Set the role - console.log(chalk.blue.bold('Setting the company\'s role...')) if (isOwner) { - myCompany.role = isOwner + myCompany.role = 'Owner' } else { const tmpRole = await this.wutils.doCheckbox( "What role should we assign to this company?", @@ -358,6 +361,7 @@ class AddCompany { ) myCompany.role = tmpRole[0] } + console.log(chalk.blue.bold(`Set the company\'s role to [${myCompany.role}]`)) this.cutils.printLine() console.log(chalk.blue.bold('Setting special properties to known values...')) @@ -370,12 +374,12 @@ class AddCompany { // TODO you need to link to one or more studies myCompany.linked_studies = {} this.cutils.printLine() - console.log(myCompany) - console.log(chalk.blue.bold(`Saving company ${myCompany.name} to mediumroast.io...`)) - return await this.apiController.createObj(myCompany) + let companyResp = await this.apiController.createObj(myCompany) + companyResp[1].data = myCompany // This might be a little hacky, but it should work + return companyResp } } -export { AddCompany } \ No newline at end of file +export default AddCompany \ No newline at end of file diff --git a/src/cli/interactionWizard.js b/src/cli/interactionWizard.js index 068b1c0..769d564 100755 --- a/src/cli/interactionWizard.js +++ b/src/cli/interactionWizard.js @@ -17,7 +17,7 @@ import chalk from 'chalk' import ora from "ora" import path from "node:path" import crypto from "node:crypto" -import { WizardUtils } from "./commonWizard.js" +import WizardUtils from "./commonWizard.js" import { Utilities } from "../helpers.js" class AddInteraction { @@ -413,7 +413,7 @@ class AddInteraction { let [myObjs, autoSuccess, autoMsg] = [{}, null, {}] if (automatic) { // Perform auto setup - console.log(chalk.blue.bold('Starting automatic company creation...')) + console.log(chalk.blue.bold('Starting automatic interaction creation...')) const [success, msg, objs] = await this.doAutomatic(interactionPrototype) myObjs = objs autoSuccess = success diff --git a/src/output.js b/src/output.js index aab9db7..81c0d14 100644 --- a/src/output.js +++ b/src/output.js @@ -11,6 +11,7 @@ import Table from 'cli-table' import Parser from 'json2csv' import * as XLSX from 'xlsx' +import logo from 'asciiart-logo' import { Utilities } from './helpers.js' class CLIOutput { diff --git a/src/s3.js b/src/s3.js index e80f625..f91e6a0 100644 --- a/src/s3.js +++ b/src/s3.js @@ -22,12 +22,12 @@ class s3Utilities { constructor(env) { this.env = env this.s3Controller = new AWS.S3({ - accessKeyId: this.env.s3User , - secretAccessKey: this.env.s3APIKey, - endpoint: this.env.s3Server , + accessKeyId: this.env.user , + secretAccessKey: this.env.api_key, + endpoint: this.env.server , s3ForcePathStyle: true, // needed with minio? signatureVersion: 'v4', - region: this.env.s3Region // S3 won't work without the region setting + region: this.env.region // S3 won't work without the region setting }) } @@ -36,7 +36,7 @@ class s3Utilities { * @description From an S3 bucket download the document associated to each interaction * @param {Array} interactions - an array of interaction objects * @param {String} targetDirectory - the target location for downloading the objects to - * @todo As the implementation grows this function will likely need be put into a separate class + * @todo this.env.s3Source is incorrect meaning it will fail for now, add srcBucket as argument */ async s3DownloadObjs (interactions, targetDirectory) { for (const interaction in interactions) {