diff --git a/README.md b/README.md index 4b380e2..83a7534 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # Node.js Javascript SDK for the mediumroast.io -This is the Node.js Javascript SDK, Software Development Kit, for the mediumroast.io. We actually use this SDK for our own developments of `web_ui`, `cli` and `text_ui` directly. We do this to ensure that our developers have a first class experience with the mediumroast.io with an always up to date version of the SDK. +This is the Node.js Javascript SDK, Software Development Kit, for the mediumroast.io. The SDK is comprised of several things: +1. A wrapper atop the backend's RESTful APIs for Interaction, Study, Company and User objects. +2. A high level written to make it easier to work with the node.js docx package and generated Microsoft Word reports. +3. Core Command Line Interface (CLI) utilities for mediumroast.io companies, studies, interactions and user objects. +4. Helper CLI utilities to setup your mediumroast.io environment and backup objects. +5. Example data to use with the CLIs to add objects to the mediumroast.io. + +We actually use this SDK for our own developments directly. We do this to ensure that our developers have a first class experience for the mediumroast.io with an always up to date version of the SDK. + +# Installation and Configuration Steps via NPM + # Installation and Configuration Steps for Developers The following steps are important if you are developing or extending the Node.js Javascript SDK. If you're not a developer then the current state of the package is not for you. Please wait until we exit the alpha for the mediumroast.io. @@ -7,266 +17,18 @@ The following steps are important if you are developing or extending the Node.js ## Cloning the repository for Developers Assuming `git` is installed and your credentials are set up to talk to the mediumroast.io set of repositories it should be possible to do the following as a user on the system: 1. `mkdir ~/dev;cd ~/dev` -2. `git clone git@github.com:mediumroast/mr_sdk.git` -This will create an `mr_sdk` directory in `~/dev/` and allow you to proceed to the following steps for installation. +2. `git clone git@github.com:mediumroast/mediumroast_js.git` +This will create an `mediumroast_js` directory in `~/dev/` and allow you to proceed to the following steps for installation. ## Installation for Developers, Early Adopters and Testers For developers the following steps are suggested to perform a local installation. 1. Install the [Local Package Publisher](https://www.npmjs.com/package/local-package-publisher) as per the instructions on the package page. We recommend that you install the package globally so that you don't have to worry about it being in the right place. If you choose to install globally you should run `sudo npm install -g local-package-publisher`. -2. Assuming that you've clone the repo into `~/dev` enter the appropriate directory `cd ~/dev/mr_sdk/javascript`. +2. Assuming that you've clone the repo into `~/dev` enter the appropriate directory `cd ~/dev/mediumroast_js`. 3. Install the package globally with `sudo local-package-publisher -p` -4. Run `npm link mediumroast` in target project to consume the library. - -## Structure of the repository -The following structure is available for the Node.js Javascript SDK. -``` -mr_sdk/ - javascript/ - cli/ - list_companies.js - list_studies.js - list_interactions.js - list_users.js - src/ - helpers.js - api/ - highLevel.js - jsonServer.js - spec/ - README.md - LICENSE - package.json - rollup.config.js -``` -# Future Work -As mentioned earlier in the documentation the current state of this package is alpha. As such we're in pretty heavy development with the high potential for things being broken. After we graduate from alpha we anticipate the following things to likely occur or be completed. -- Separation of the package out of the `mr_sdk` and into separate `python`, `javascript` and `cli` packages. -- For this Node.js Javascript package it will be published on the `npm` repository. -- For the Python package it will be published on `PyPi` or similar. -- Implementation of a test suite to perform object adds, updates, reads, and deletes to confirm basic operation. -- Implementation of `put`, `patch`, and `delete` operations to perform full object functions. -- Creation of correlating `cli` commands or a `tui` for updates, deletes and adds. -- Swap out the temporary backend for the final `mr_server` backend. - -# The CLI, Command Line Interface -The following example CLI wrappers have been built that wrap the sample API implementation and other elements in the SDK. As appropriate example outputs are also included in the documentation. -## list_companies -``` -Usage: list_companies [options] - -A CLI for mediumroast.io Company objects, without options: list all Companies. - -Options: - -V, --version output the version number - -g --get_guids List all Companies by Name - -n --get_names List all Companies by GUID - -m --get_map List all Companies by {Name:GUID} - --get_by_name Get an individual Company by name - --get_by_guid Get an individual Company by GUID - -s --server Specify the server URL (default: "http://mr-01:3000") - -t --server_type Specify the server type as [json || mr_server] (default: "json") - -c --config_file Path to the configuration file (default: "~/.mr_config") - -h, --help display help for command -``` -### Example output -``` -list_companies --get_by_guid=70340e90b851a6f1e4dd725cfa22b533d0e3eecd457a1e23fe51be56d9a75d76 -[ - { - companyName: 'JP Morgan Chase', - industry: 'Finance, Insurance, And Real Estate | Major Group 60: Depository Institutions | National Commercial Banks', - role: 'User', - url: 'Unknown', - streetAddress: 'Unknown', - city: 'San Francisco', - stateProvince: 'California', - country: 'US', - region: 'AMER', - phone: 'Unknown', - simpleDesc: 'JPMorgan Chase & Co. is an American multinational investment bank and financial services holding company headquartered in New York City.', - cik: 'Unknown', - stockSymbol: 'Unknown', - Recent10kURL: 'Unknown', - Recent10qURL: 'Unknown', - zipPostal: 'Unknown', - linkedStudies: { - 'Caffeine Customer Insights': 'dccd4e2568bfd5733d3ef9f3f8dad09ea0da0e87325ccafeac1abfd0c52c23ff', - 'Customer Insights': 'f3eae874b1fba924e81d5963a2bc7752ab8d2acd906bb2944f6243f163a6bf23' - }, - linkedInteractions: { - '202108091407-Caffeine Customer Insights-JP Morgan Chase': '3206b19fa72cde9970ceb3792b1f2ea54afa69c7d80adc74d07fb9f699db860e', - '202108091407-Customer Insights-JP Morgan Chase': 'd579da261a38581f3e3b5c800d7e956f3f22570cf8a71dfd5e4e45f45a7ea3dd' - }, - longitude: -122.41963999999996, - latitude: 37.777120000000025, - notes: { '1': [Object] }, - GUID: '70340e90b851a6f1e4dd725cfa22b533d0e3eecd457a1e23fe51be56d9a75d76', - id: '70340e90b851a6f1e4dd725cfa22b533d0e3eecd457a1e23fe51be56d9a75d76', - totalInteractions: 2, - totalStudies: 2 - } -] -``` -## list_interactions -``` -Usage: list_interactions [options] +4. Run `npm link mediumroast_js` in target project to consume the library. -A CLI for mediumroast.io Interaction objects, without options: list all Interactions. -Options: - -V, --version output the version number - -g --get_guids List all Interactions by Name - -n --get_names List all Interactions by GUID - -m --get_map List all Interactions by {Name:GUID} - --get_by_name Get an individual Interaction by name - --get_by_guid Get an individual Interaction by GUID - -s --server Specify the server URL (default: "http://mr-01:3000") - -t --server_type Specify the server type as [json || mr_server] (default: "json") - -c --config_file Path to the configuration file (default: "~/.mr_config") - -h, --help display help for command -``` -### Example output -``` -list_interactions --get_by_guid=729fbd901aaa7114ea46df0b8736fc599d8d7b94f5f9c74375ebd684c4d349cf -[ - { - interactionName: '202108111451-Customer Insights-Self Employed', - time: '1451', - date: '20210811', - state: 'summarized', - simpleDesc: 'Learn from Self Employed, either in person or digitally, key points and inputs related to the study Customer Insights', - contactAddress: 'Unknown', - contactZipPostal: 'Unknown', - contactPhone: 'Unknown', - contactLinkedIn: 'Unknown', - contactEmail: 'Unknown', - contactTwitter: 'Unknown', - contactName: 'Unknown', - public: false, - abstract: "Audio le Wow there's some new boys that says. Nice so it is a. It's a one man shop one man. 00:058 Speaker 2 In that era and then following people on Twi er that are interested in this type of game to see what excites them. You're trying to get insights on on you know Twi er or talking to people that may already have the consoles or they may have them haven't used them and maybe they would like to go back to that por on right? Alright so of course I cannot ask you what well this is a good ques on so over me do you feel that your company Is losing or maintaining its as the essence of your product? And I'm being serious about this kind of thing. Uhm a ec vely what I've what I did was decided I wanted to go a er. So I had to build you know I'm doing a pla orm or Mario Castlevania type game so I had to build you know physics and collision detec on and you know all that kind of stu from the ground up. My rst objec ve was to make a. List of all. And and by the me you're done you go back and look at your original your original goal and you're like how did we even get here? Do you ever felt that product management or PM UM were asked to put together a road map without having customer insight? So it's kind of a hard ques on to answer 'cause. No you know you know the three right right? I think for some context I feel like the problem got worse over me. So you you have a plan for when you're going to deliver stu but you probably didn't do. Do we do a great job of saying yeah we know we're going to need to allocate such so much percent of our of our me to. Alright cool this is pre y good evidence.", - interactionType: 'Interview', - status: 'Canceled', - linkedStudies: { - 'Customer Insights': 'f3eae874b1fba924e81d5963a2bc7752ab8d2acd906bb2944f6243f163a6bf23' - }, - linkedCompanies: { - 'Self Employed': '7c916f300b766bab9c9652f1e83617d0e3488a810b3a9388573905c52f924f84' - }, - longitude: -80.83795999999995, - latitude: 35.222860000000026, - url: 's3://mr-02:9000/mediumroastinc/202108111451-AMER-US-North Carolina-Charlotte-Entertainment-Customer Insights-Self Employed-Interview.pdf', - thumbnail: 's3://mr-02:9000/mediumroastinc/thumb_202108111451-AMER-US-North Carolina-Charlotte-Entertainment-Customer Insights-Self Employed-Interview.pdf.png', - notes: { '1': [Object] }, - GUID: '729fbd901aaa7114ea46df0b8736fc599d8d7b94f5f9c74375ebd684c4d349cf', - id: '729fbd901aaa7114ea46df0b8736fc599d8d7b94f5f9c74375ebd684c4d349cf', - totalStudies: 1, - totalCompanies: 1 - } -] -``` -## list_studies -``` -Usage: list_studies [options] +# The mediumroast.io CLI (Command Line Interface) -A CLI for mediumroast.io Study objects, without options: list all Studies. -Options: - -V, --version output the version number - -g --get_guids List all Studies by Name - -n --get_names List all Studies by GUID - -m --get_map List all Studies by {Name:GUID} - --get_substudies List all Studies and their substudies - --get_by_name Get an individual Study by name - --get_by_guid Get an individual Study by GUID - -s --server Specify the server URL (default: "http://mr-01:3000") - -t --server_type Specify the server type as [json || mr_server] (default: "json") - -c --config_file Path to the configuration file (default: "~/.mr_config") - -h, --help display help for command -``` -### Example output -``` -list_studies --get_by_guid=f3eae874b1fba924e81d5963a2bc7752ab8d2acd906bb2944f6243f163a6bf23 -[ - { - studyName: 'Customer Insights', - description: 'Work to realize a SaaS intently focused on revealing and solving problems for Customer and\n' + - 'Competitive interactions which can make Customer Success and Product Management disciplines stronger.', - linkedCompanies: { - HDS: 'b61e57adaaa834f2a560be1b8d1b62c0ebbfd68cb3d33917185bc1792063a677', - Aha: '6dbfa33b06706033931b0154210fbcb5fafb995315eccfbd8bc5b12d5e5569f7', - VMware: 'd9069370a7dc4fff8ef20bf01915df2568bb26dee54c54f209664c089843f0e3', - Hitachi: '567922c545773872eaab4b5e3466e0a60756d98148849662be330ff336b56659', - Google: '392dfcd311a276b9aca48dfa52b5985aa5546c3157c3288452bbc00f2af8048a', - Microsoft: '6ffc56686598cbac08864b9218f43e203b22f258d00ab7dc3e4a1aa29819f982', - Amazon: 'df6cb4f48281f2612c6ca5e9d89f85a684c39e3aa25ab648ba91db60368dfe8d', - 'Providence Health and Services': 'ca78cf0b874c9139b0316c1803408553202069186297a8bec7bfea862168032b', - eBay: 'ed128e1b2763deba9a49583d91a1dafd979f2453fed920339fe98e7d30e66aa7', - 'JP Morgan Chase': '70340e90b851a6f1e4dd725cfa22b533d0e3eecd457a1e23fe51be56d9a75d76', - 'Self Employed': '7c916f300b766bab9c9652f1e83617d0e3488a810b3a9388573905c52f924f84' - }, - totalCompanies: 11, - linkedInteractions: { - '201402240930-Customer Insights-HDS': '37c5b453fdc2a4074958c3c41f02c2491f9961eafbcdc30a354b586e379a94f4', - '201402241004-Customer Insights-HDS': 'e216d44c6b934beebf1cf58a27030233c3e5e9ea8e5fc457c3003f99dc54efe7', - '201402250831-Customer Insights-HDS': 'bd5b40cb911347abb82bc95c59105e7ea1d3ac48248f0644aa348ab5399bd4d3', - '201402250915-Customer Insights-HDS': '57bf64ca80c491324e917cbeda55d6fe7494c9c8c9e09033a9648e6d4e1cd640', - '201402270900-Customer Insights-HDS': 'af98040145afe692ae78b806323289ad5fddb99c5f45d453d8fc5dde2292a8a9', - '201402271515-Customer Insights-HDS': '0928f03c7b34f90333eacd162fe8ba1dc748639d1cd361613e70de6253222322', - '201402281310-Customer Insights-HDS': '76d41347eba7ddf45aaa7ccf9e40d204e3b0f643ede6d3fc4d212af8c0ffa7de', - '201403101041-Customer Insights-HDS': 'c1eaca9b8de83997b81c2143479b78b5b705c878ef3f1610257278691fdf95d4', - '201403110945-Customer Insights-HDS': '1c992ed623c0ce6ec01470a3438720c39198c0153dcc38be24429a42137dd3be', - '201403111306-Customer Insights-HDS': '1f15ec408b0857662152566408118b580734684f1e7dca5a6d7a77f84cffb041', - '201403121034-Customer Insights-HDS': '60d72cc0a3f0837a81edeec80b8673895ab076d52753d4707115d6f81a239883', - '201403130801-Customer Insights-HDS': '43ac78317d2c402abe0b9afd942e1e742e4029ea3a85679bde2cb740f3c697db', - '201403140702-Customer Insights-HDS': '1fe08d24f23e303d70f73b438b33556df190ee7c2edb076879f9a15f676ee558', - '201912151800-Customer Insights-Aha': 'd42ba72ab7317dea495dbef9fd7a4b18e220316689059d5ce87134a68033ed6a', - '201912151900-Customer Insights-Aha': '2975c26e6ac058eaa6617949e4be59c14ed8da69d49168f2b7e11eedc7ff689c', - '201912152000-Customer Insights-Aha': '03d225af95bc8d9ae1af795cae25b1f98afd886d3772e7b56b1899642fdbf8fc', - '201912161800-Customer Insights-Aha': '203719812f4d2b7d986f134c81c7e01ff0b7ec0a454351ac020b3fc23ee7209f', - '201912161900-Customer Insights-Aha': '9c9703bd9ea5aa58fbec75da5b7a67848a2c69efb34d6b82bdd43f4dca700c1a', - '201912162000-Customer Insights-Aha': '440a968c8ec519d046a2064e4c1704307499f013eca6fae639cebdc6a1d31080', - '201912171800-Customer Insights-Aha': 'cf22d4ac4c07fa502fdf507237396cc919b99ca6d5e20ede53fc0c65c3657886', - '201912171900-Customer Insights-Aha': 'adcdc8c0c4e27ef6617cf61927253473986103662e9b1a0c7e3d0b4c93de41f2', - '201912172000-Customer Insights-Aha': '02e5fbd561ae1ab679b793644c5bcf50fb26163623e35c2ff1a76f44f1022a46', - '201912181800-Customer Insights-Aha': '29581d790d073ab20b1d7814f6c777944b601d038a59a5a9553f47eb29aa20c7', - '201912181900-Customer Insights-Aha': '2599cac8d57cc536b8f46b55f4b90a265af000faf09785aebf37303179dbc61b', - '201912182000-Customer Insights-Aha': '70cd002264a60361fcea43bfeb5bca9b6cfc05364b377530581a7317cbad69e4', - '201912191800-Customer Insights-Aha': 'a0ec3c225c1b5a6df8444955c56d458b6920a91df7417a4a8b19babfd223401d', - '201912191900-Customer Insights-Aha': '167c7dc72ed236c70c165e510de251d635d20116922068e8dc4579d6277e2725', - '201912192000-Customer Insights-Aha': 'c812a6f111acfb353ba944f3c536334a1c15fb2663c3dd62ca6e0f78208eaf7f', - '201912201800-Customer Insights-Aha': '02e4f0587d9426d4d9aa26d88eff656b35058c250ee58703ecb88e187bc6ca31', - '201912201900-Customer Insights-Aha': '7f0b7da04a4919c7bc4a2e676fb916e0cc1465deaeadc9d74008baf28268c74c', - '201912202000-Customer Insights-Aha': 'fe57ff2b168359e14950b6f46c45a891494b90651adf784096ecd7d4fdd0f003', - '201912211800-Customer Insights-Aha': 'cdb15ae893120080c7cef65f23c0a7f0ed16942077a6b3af7be1f2c6fc8fe63a', - '201912211900-Customer Insights-Aha': '60438bac13b7fab03a9d48062b1764ffb627acec94403c0eec79185d6771b448', - '201912212000-Customer Insights-Aha': '1255026e832da09862cf55623ea0a78d795aa10a12522860e7720c6e3664de16', - '201912212100-Customer Insights-Aha': 'a1049f53de0929252fd6b655be1da37ae9ed27fe44f2814b5f0f9e102b7cabd3', - '202107091400-Customer Insights-VMware': 'f99a403173fed358fc7d6937ba3d646412e248fabd6b20280a9099121e0ca121', - '202107231300-Customer Insights-Hitachi': 'd63337f0967ef5c84953b9ca0b2023cf90da3dcc401baf91fc43687826bd6c99', - '202107281900-Customer Insights-Google': 'c9a9094844a42394ee61b5e35572701a128815fc01047282e53d315134ce9eac', - '202107301345-Customer Insights-Microsoft': '13c0455ce917a0ced3047ac93b52b649c0d17ae124eb8a6067142579b178cd2f', - '202108031722-Customer Insights-Amazon': '1f5252dbab6cdebc669d3c6e306f73c4f0562dbadda7299e795f78d9334ee6a1', - '202108041019-Customer Insights-Providence Health and Services': '7490db9c62b688cffa865569d6bc5504bbc2dc8ac5ddb5919b21e0a5f65eb15c', - '202108051509-Customer Insights-eBay': '0aa63c14ca41f7f422f569f5dab55c2674d83d9c43a18013fcbb641130ef1e1b', - '202108091407-Customer Insights-JP Morgan Chase': 'd579da261a38581f3e3b5c800d7e956f3f22570cf8a71dfd5e4e45f45a7ea3dd', - '202108111451-Customer Insights-Self Employed': '729fbd901aaa7114ea46df0b8736fc599d8d7b94f5f9c74375ebd684c4d349cf' - }, - totalInteractions: 44, - substudies: { '1': [Object], '2': [Object], default: [Object] }, - document: { - Introduction: 'This Customer Insights study includes two phases separated by 5 years. The first phase was performed as as a part of a 2014 research project that emphasized A/B testing for a customer study indexing application.\n' + - 'While the second phase, conducted in late 2019, both uncovered new themes and validated key ideas surfaced in the first phase. In the second phase the emphasis was to investigate a single competitor/partner candidate, Aha!, to\n' + - 'to determine if the key themes, detected within the first phase, had or had not been already addressed. While details are accounted for in the Opportunities section, the conclusion is that the themes still largely remain unsolved\n' + - 'by companies who build tools for product management, project management, and program management disciplines. Further, research continues to test both the user experience and refine elements of these\n' + - 'key themes with product managers at companies like Ring Central, Google, Chaos Search, and so on.', - Opportunity: [Object], - Action: [Object] - }, - public: false, - groups: 'users:studyadmin', - GUID: 'f3eae874b1fba924e81d5963a2bc7752ab8d2acd906bb2944f6243f163a6bf23', - id: 'f3eae874b1fba924e81d5963a2bc7752ab8d2acd906bb2944f6243f163a6bf23' - } -] -``` diff --git a/cli/company.js b/cli/company.js index 0c57ae1..3abcb14 100755 --- a/cli/company.js +++ b/cli/company.js @@ -6,24 +6,29 @@ * @file company.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @version 2.0.0 */ // Import required modules import { Auth, Companies, Interactions } from '../src/api/mrServer.js' -import { CLI } from '../src/helpers.js' +import { Utilities } from '../src/helpers.js' +import { CLIUtilities } from '../src/cli.js' import { CompanyStandalone } from '../src/report/companies.js' // Globals const objectType = 'Companies' // Construct the CLI object -const myCLI = new CLI ( +const myCLI = new CLIUtilities ( '2.0', 'company', 'Command line interface for mediumroast.io Company objects.', objectType ) +// Construct the Utilities object +const utils = new Utilities(objectType) + // Create the environmental settings const myArgs = myCLI.parseCLIArgs() const myConfig = myCLI.getConfig(myArgs.conf_file) @@ -74,7 +79,7 @@ if (myArgs.report) { if(myArgs.package) { // Create the working directory - const [dir_success, dir_msg, dir_res] = myCLI.safeMakedir(baseDir + '/interactions') + const [dir_success, dir_msg, dir_res] = utils.safeMakedir(baseDir + '/interactions') // If the directory creations was successful download the interaction if(dir_success) { @@ -88,7 +93,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 myCLI.s3DownloadObjs(interactions, myEnv, baseDir + '/interactions') + await utils.s3DownloadObjs(interactions, myEnv, baseDir + '/interactions') // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) @@ -97,7 +102,7 @@ if (myArgs.report) { } // Create the document - const [report_success, report_stat, report_result] = await docController.makeDocx(fileName, myArgs.package) + const [report_success, report_stat, report_result] = await docController.makeDOCX(fileName, myArgs.package) // Create the package and cleanup as needed if (myArgs.package) { diff --git a/cli/doc_company.js b/cli/doc_company.js deleted file mode 100755 index 2f84133..0000000 --- a/cli/doc_company.js +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env node - -// Import required modules -import { Companies, Interactions, Studies } from '../src/api/highLevel.js' -import Firmographics from '../src/report/company.js' -import Utilities from '../src/report/common.js' -import program from 'commander' -import ConfigParser from 'configparser' -import * as fs from "fs" -import docx from 'docx' -import AWS from 'aws-sdk' -import zip from 'adm-zip' -import boxPlot from 'box-plot' - - -// ______ __ __ __ __ ______ ______ __ ______ __ __ ______ -// /\ ___\ /\ \/\ \ /\ "-.\ \ /\ ___\ /\__ _\ /\ \ /\ __ \ /\ "-.\ \ /\ ___\ -// \ \ __\ \ \ \_\ \ \ \ \-. \ \ \ \____ \/_/\ \/ \ \ \ \ \ \/\ \ \ \ \-. \ \ \___ \ -// \ \_\ \ \_____\ \ \_\\"\_\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_\\"\_\ \/\_____\ -// \/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_____/ - -// Parse the cli options -function parseCLIArgs() { - // Define commandline options - program - .version('0.7.5') - .description('A CLI to generate a document report for mediumroast.io Company objects.') - program - .requiredOption('-n --name ', 'The name of the company to construct a report for.') - .option('-s --substudy ', 'The GUID for the substudy to include in the report.') - .option('-r --report_dir ', 'Directory to write the report to', 'Documents') - .option('-w --work_dir ', 'Directory to use for creating a ZIP package', '~/Documents') - .option('-p --package', 'Create a package with interactions') - .option('-z --zip', 'Create a ZIP archive with report and interactions') - .option('-s --server ', 'Specify the server URL', 'http://mr-01:3000') - .option('-t --server_type ', 'Specify the server type as [json || mr_server]', 'json') - .option('-a --author_company ', 'Specify the company the report is for') - .option('-c --config_file ', 'Path to the configuration file', '.mr_config') - program.parse(process.argv) - const options = program.opts() - return options -} - -// Filter interactions by the GUID of the Company -function filterObjects(objects, guid) { - let myObjects = [] - for (const object in objects) { - const allCompanies = Object.values(objects[object].linkedCompanies) - if (allCompanies.includes(guid)) { - myObjects.push(objects[object]) - } - } - return myObjects -} - -function rankTags (tags, ranges) { - let finalTags = {} - for (const tag in tags) { - // Rank the tag score using the ranges derived from box plots - // if > Q3 then the ranking is high - // if in between Q2 and Q3 then the ranking is medium - // if < Q3 then the ranking is low - let rank = null - if (tags[tag] > ranges.upperQuartile) { - rank = 'High' - } else if (tags[tag] < ranges.lowerQuartile) { - rank = 'Low' - } else if (ranges.lowerQuartile <= tags[tag] <= ranges.upperQuartile) { - rank = 'Medium' - } - - finalTags[tag] = { - score: tags[tag], // Math.round(tags[tag]), - rank: rank - } - - } - return finalTags -} - -// ______ ______ __ __ ______ __ ______ -// /\ ___\ /\ __ \ /\ "-.\ \ /\ ___\ /\ \ /\ ___\ -// \ \ \____ \ \ \/\ \ \ \ \-. \ \ \ __\ \ \ \ \ \ \__ \ -// \ \_____\ \ \_____\ \ \_\\"\_\ \ \_\ \ \_\ \ \_____\ -// \/_____/ \/_____/ \/_/ \/_/ \/_/ \/_/ \/_____/ - -// Get the configuration objects -const opts = parseCLIArgs() // CLI arguments and options -const config = new ConfigParser() // Config file -config.read(process.env.HOME + '/' + opts.config_file) - -// Set the server type -let serverType = null -config.hasKey('DEFAULT', 'server_type') ? serverType = config.get('DEFAULT', 'server_type') : serverType = opts.server_type - -// Set the server url -let mrServer = null -config.hasKey('DEFAULT', 'server') ? mrServer = config.get('DEFAULT', 'server') : mrServer = opts.server - -// Set the working directory -let workDir = null -config.hasKey('DEFAULT', 'working_dir') ? workDir = config.get('DEFAULT', 'working_dir') : workDir = opts.work_dir - -// Set the output directory -let outputDir = null -config.hasKey('DEFAULT', 'output_dir') ? outputDir = process.env.HOME + '/' + config.get('DEFAULT', 'output_dir') : outputDir = process.env.HOME + '/' + opts.output_dir - -// Set the author company -let authorCompany = null -config.hasKey('DEFAULT', 'company') ? authorCompany = config.get('DEFAULT', 'company') : outputDir = opts.author_company - -// Set up the S3 credentials and download protocol -const s3Server = config.get('s3_credentials', 'server') -const s3User = config.get('s3_credentials', 'user') -const s3APIkey = config.get('s3_credentials', 'api_key') -const s3Source = config.get('s3_credentials', 'source') -const s3Region = config.get('s3_credentials', 'region') -const s3Protocol = 's3' -const localProtocol = 'file' -const httpProtocol = 'http' - -// Determine if we need to create a zip package or not -let createPackage = null -opts.package ? createPackage = true : createPackage = false - -// Determine if we need to create a zip package or not -let createArchive = null -opts.zip ? createArchive = true : createArchive = false - -// Obtain the GUID for the needed substudy -let addedSubstudy = null -opts.substudy ? addedSubstudy = opts.substudy : addedSubstudy = false - -// Set up the control objects -const companyCtl = new Companies(mrServer, serverType) -const interactionCtl = new Interactions(mrServer, serverType) -const studyCtl = new Studies(mrServer, serverType) -const docCtl = new Utilities( - config.get('document_settings', 'font_type'), - parseInt(config.get('document_settings', 'font_size')), - parseInt(config.get('document_settings', 'title_font_size')), - config.get('document_settings', 'title_font_color') -) -const s3Ctl = new AWS.S3({ - accessKeyId: s3User , - secretAccessKey: s3APIkey , - endpoint: s3Server , - s3ForcePathStyle: true, // needed with minio? - signatureVersion: 'v4', - region: s3Region // S3 won't work without the region setting -}) - -// Get the company in question and all interactions -const company = await companyCtl.getByName(opts.name) -const interactions = filterObjects(await interactionCtl.getAll(), company[0].GUID) - -// Get the relevant study and substudy -const [studyName, substudyId] = opts.substudy.split(':') -const studies = await studyCtl.getByName(studyName) -const substudy = studies[0].substudies[substudyId] -const ranges = boxPlot(Object.values(substudy.keyThemes.summary_theme.tags)) -const quotes = (substudy.keyThemeQuotes.summary) -const tags = rankTags(substudy.keyThemes.summary_theme.tags, ranges) - -// simple function for safe directory creation -function safeMakedir(name) { - try { - if (!fs.existsSync(name)) { - fs.mkdirSync(name) - } - } catch (err) { - console.error(err) - } -} - -// create a ZIP package -async function createZIPArchive -(outputFile, sourceDirectory) { - try { - const zipPackage = new zip(); - zipPackage.addLocalFolder(sourceDirectory); - zipPackage.writeZip(outputFile); - console.log(`Created ${outputFile} successfully`); - } catch (e) { - console.log(`Something went wrong. ${e}`); - } -} - -function writeReport (docObj, fileName) { - docx.Packer.toBuffer(docObj).then((buffer) => { - fs.writeFileSync(fileName, buffer) - }) -} - -// Download the objects -async function downloadInteractions (interactions, directory) { - for (const interaction in interactions) { - const objWithPath = interactions[interaction].url.split('://').pop() - const myObj = objWithPath.split('/').pop() - const myParams = {Bucket: s3Source, Key: myObj} - const myFile = fs.createWriteStream(directory + myObj) - s3Ctl.getObject(myParams). - on('httpData', function(chunk) { myFile.write(chunk) }). - on('httpDone', function() { myFile.end() }). - send() - } -} - -// __ __ ______ __ __ __ ______ __ __ -// /\ "-./ \ /\ __ \ /\ \ /\ "-.\ \ /\ ___\ /\ \ /\ \ -// \ \ \-./\ \ \ \ __ \ \ \ \ \ \ \-. \ \ \ \____ \ \ \____ \ \ \ -// \ \_\ \ \_\ \ \_\ \_\ \ \_\ \ \_\\"\_\ \ \_____\ \ \_____\ \ \_\ -// \/_/ \/_/ \/_/\/_/ \/_/ \/_/ \/_/ \/_____/ \/_____/ \/_/ - -const outputFile = outputDir + '/' + company[0].companyName -const outputDocFileName = outputFile + '.docx' - - -// Set key properties for the document -const creator = 'mediumroast.io barista robot' -const title = company[0].companyName + ' Company Report' -const description = 'A report snapshot including firmographics and interactions for: ' - + company[0].companyName - -// Get the first page for the company that includes firmographics -const companyData = new Firmographics(company, interactions, 'Interactions/', tags, quotes) - -let doc = new docx.Document ({ - creator: creator, - company: authorCompany, - title: title, - description: description, - styles: {default: docCtl.styling.default}, - numbering: docCtl.styling.numbering, - sections: [{ - properties: {}, - children: companyData.companyDoc, - }], -}) - -// If needed create the zip package -if (createPackage) { - - const fileName = 'Company Report.docx' - const interactionsDir = 'Interactions/' - - // As needed create the working directory - const workingDirectory = workDir + '/' + company[0].companyName + '/' - safeMakedir(workingDirectory) - safeMakedir(workingDirectory + interactionsDir) - - // Write the report to the working directory - writeReport(doc, workingDirectory + fileName) - - // Download the objects - await downloadInteractions(interactions, workingDirectory + interactionsDir) - -} else if (createArchive) { - const outputPackage = outputFile + '.zip' - // Create the zip package - await createZIPArchive(outputPackage, workDir + '/' + company[0].companyName) -} else { - writeReport(doc, outputDocFileName) -} \ No newline at end of file diff --git a/cli/interaction.js b/cli/interaction.js index c35d900..224ce75 100755 --- a/cli/interaction.js +++ b/cli/interaction.js @@ -6,23 +6,26 @@ * @file interactions.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @version 2.0.0 */ // Import required modules import { Auth, Interactions, Companies } from '../src/api/mrServer.js' -import { CLI } from '../src/helpers.js' +import { CLIUtilities } from '../src/cli.js' +import { Utilities } from '../src/helpers.js' import { InteractionStandalone } from '../src/report/interactions.js' // Globals const objectType = 'Interactions' // Construct the CLI object -const myCLI = new CLI( +const myCLI = new CLIUtilities( '2.0', 'interaction', 'Command line interface for mediumroast.io Interaction objects.', objectType ) +const utils = new Utilities(objectType) // Create the environmental settings const myArgs = myCLI.parseCLIArgs() @@ -68,7 +71,7 @@ if (myArgs.report) { if(myArgs.package) { // Create the working directory - const [dir_success, dir_msg, dir_res] = myCLI.safeMakedir(baseDir + '/interactions') + const [dir_success, dir_msg, dir_res] = utils.safeMakedir(baseDir + '/interactions') // If the directory creations was successful download the interaction if(dir_success) { @@ -82,7 +85,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 myCLI.s3DownloadObjs(int_results, myEnv, baseDir + '/interactions') + await utils.s3DownloadObjs(int_results, myEnv, baseDir + '/interactions') // Else error out and exit } else { console.error('ERROR (%d): ' + dir_msg, -1) @@ -91,17 +94,17 @@ if (myArgs.report) { } // Create the document - const [report_success, report_stat, report_result] = await docController.makeDocx(fileName, myArgs.package) + const [report_success, report_stat, report_result] = await docController.makeDOCX(fileName, myArgs.package) // Create the package and cleanup as needed if (myArgs.package) { - const [package_success, package_stat, package_result] = await myCLI.createZIPArchive( + const [package_success, package_stat, package_result] = await utils.createZIPArchive( myEnv.outputDir + '/' + baseName + '.zip', baseDir ) if (package_success) { console.log(package_stat) - myCLI.rmDir(baseDir) + utils.rmDir(baseDir) process.exit(0) } else { console.error(package_stat, -1) diff --git a/cli/mr_backup.js b/cli/mr_backup.js index 7321095..bb0bc8c 100644 --- a/cli/mr_backup.js +++ b/cli/mr_backup.js @@ -1,5 +1,13 @@ #!/usr/bin/env node +/** + * A CLI utility to backup and restore data from the mediumroast.io + * @author Michael Hay + * @file mr_backup.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + */ + // Import required modules import { Auth, Companies, Interactions, Studies, Users } from '../src/api/mrServer.js' import CLI from '../src/helpers.js' diff --git a/cli/study.js b/cli/study.js index ac33313..98818bc 100755 --- a/cli/study.js +++ b/cli/study.js @@ -6,8 +6,13 @@ * @file study.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @verstion 2.0.0 */ +// TODO: This needs to be reimplemented using the right structure as the other CLIs +console.log('NOTICE: This CLI is presently a work in progress and will not operate, exiting.') +process.exit(0) + // Import required modules import { Auth, Studies } from '../src/api/mrServer.js' import { CLI } from '../src/helpers.js' diff --git a/cli/users.js b/cli/users.js index 6f727f9..70e2bf0 100755 --- a/cli/users.js +++ b/cli/users.js @@ -1,5 +1,9 @@ #!/usr/bin/env node +// TODO: This needs to be reimplemented using the right structure as the other CLIs +console.log('NOTICE: This CLI is presently a work in progress and will not operate, exiting.') +process.exit(0) + // Import required modules import {Users} from '../src/api/highLevel.js' import program from 'commander' diff --git a/package.json b/package.json index e472ed4..a0cf2a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediumroast_js", "version": "0.3.0", - "description": "A Javascript SDK to interaction with the mediumroast.io application, end user command line interfaces and a a series of CLIs for building company, study and interaction reports.", + "description": "A Javascript SDK to interact with the mediumroast.io application including command line interfaces.", "main": "src/api/mrServer", "typings": "dist/index", "scripts": { @@ -14,7 +14,7 @@ "company": "cli/company.js", "interaction": "cli/interaction.js", "users": "cli/users.js", - "doc_company": "cli/doc_company.js" + "mr_backup": "cli/mr_backup.js" }, "type": "module", "keywords": [ diff --git a/rollup.config.js b/rollup.config.js index 662cacd..227ab76 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,7 @@ export default [ { input: "src/index.js", // your entry point output: { - name: "mediumroast", // package name + name: "mediumroast_js", // package name file: pkg.browser, format: "umd", }, diff --git a/src/api/mrServer.js b/src/api/mrServer.js index b5ba309..c241615 100644 --- a/src/api/mrServer.js +++ b/src/api/mrServer.js @@ -4,12 +4,25 @@ * @file mrServer.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @version 1.0.0 */ // Import required modules import mrRest from './scaffold.js' class Auth { + /** + * The present development is very simple and largely a placeholder. After the object is constructed + * the user would issue a login to generate the credential for usage in the API to talk to the + * mediumroast.io application and gather create, read, deleted and update various objects. + * @constructor + * @classdesc An implementation for authenticating into the mediumroast.io application. + * @param {String} restServer - the full URL and TCP/IP port for the mediumroast.io application + * @param {String} apiKey - the API key for the mediumroast.io application + * @param {String} user - your username for the mediumroast.io application + * @param {String} secret - your secret for the mediumroast.io application + * @todo Evolve as the backend improves authentication and authorization + */ constructor(restServer, apiKey, user, secret) { this.apiKey = apiKey this.user = user @@ -17,6 +30,11 @@ class Auth { this.restServer = restServer } + /** + * @function login + * @description Initiate a login to the mediumroast.io application + * @returns {Object} the credential object needed to perform API calls to mediumroast.io + */ login() { return { 'apiKey': this.apiKey, @@ -26,12 +44,31 @@ class Auth { } } + /** + * @function logout + * @description While not yet implemented meant to enable a logout of a session for the mediumroast.io + * @returns {Boolean} true for logout at this time + */ logout() { return true } } class baseObjects { + /** + * This class contains all of the core operations which make it easier to interact with + * the mediumroast.io application. Access to the RESTful endpoints is wrapped in a series + * of Javascript functions to enable SDK users to not have to manage the details. + * + * Discrete objects are subclasses of this baseObjects class and are defined below. These + * subclasses specify additional information like the objType, etc. and as needed can + * implement their own additional functions as needed. + * @constructor + * @classdesc Store and setup key variables needed to operate the SDK + * @param {Object} credential - the credential object returned from Auth.login() + * @param {String} objType - the type of object for the API session which could be users, studies, interactions or companies + * @param {String} apiVersion - the version of the API + */ constructor(credential, objType, apiVersion = 'v1') { this.cred = credential this.rest = new mrRest(credential) @@ -39,68 +76,143 @@ class baseObjects { this.apiVersion = apiVersion } + /** + * @async + * @function getAll + * @description Get all objects from the mediumroast.io application + * @param {String} endpoint - defaults to getall and is combined with credential and version info + * @returns {Array} the results from the called function mrRest class + */ async getAll(endpoint='getall') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint return this.rest.getObj(fullEndpoint) } + /** + * @async + * @function findByName + * @description Find all objects by name from the mediumroast.io application + * @param {String} name - the name 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 + */ async findByName(name, endpoint='findbyx') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint const my_obj = {findByX: 'name', xEquals: name} return this.rest.postObj(fullEndpoint, my_obj) } - // TODO change to findById - // NOTE this needs to change in the backend implementation too + /** + * @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 + */ async findById(id, endpoint='findbyx') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint const my_obj = {findByX: "id", xEquals: id} return this.rest.postObj(fullEndpoint, my_obj) } - // TODO change to findByX - // NOTE this needs to change in the backend implementation too + /** + * @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 + * @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 findByX(attribute, value, endpoint='findbyx') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint const my_obj = {findByX: attribute, xEquals: value} return this.rest.postObj(fullEndpoint, my_obj) } + /** + * @async + * @function createObj + * @description Create objects in the mediumroast.io application + * @param {Object} obj - the object to create in the backend + * @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 createObj(obj, endpoint='register') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint return this.rest.postObj(fullEndpoint, obj) } - + + /** + * @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(obj, endpoint='update') { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint return this.rest.postObj(fullEndpoint, obj) } + /** + * @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(id, endpoint) { const fullEndpoint = '/' + this.apiVersion + '/' + this.objType + '/' + endpoint return false } } + class Users extends baseObjects { + /** + * @constructor + * @classdesc A subclass of baseObjects that construct the user objects + * @param {Object} credential - the credential object returned from Auth.login() + */ constructor (credential) { super(credential, 'users') } } class Studies extends baseObjects { + /** + * @constructor + * @classdesc A subclass of baseObjects that construct the study objects + * @param {Object} credential - the credential object returned from Auth.login() + */ constructor (credential) { super(credential, 'studies') } } class Companies extends baseObjects { + /** + * @constructor + * @classdesc A subclass of baseObjects that construct the company objects + * @param {Object} credential - the credential object returned from Auth.login() + */ constructor (credential) { super(credential, 'companies') } } + class Interactions extends baseObjects { + /** + * @constructor + * @classdesc A subclass of baseObjects that construct the interaction objects + * @param {Object} credential - the credential object returned from Auth.login() + */ constructor (credential) { super(credential, 'interactions') } diff --git a/src/api/scaffold.js b/src/api/scaffold.js index 125515e..06175b8 100644 --- a/src/api/scaffold.js +++ b/src/api/scaffold.js @@ -4,19 +4,23 @@ * @file scaffold.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @version 1.0.0 */ // Import required modules import axios from "axios" -/** - * Simple and safe wrappers around axios to make RESTful API to mediuroast.io. - * The credential object, passed when this object is created, should include all relevant items - * needed to authenticate a client. This can include appropriate JWT tokens, user identifiers, - * passwords, etc. At a minimum the restServer and an apiKey are needed to connect. - * @class - */ + class mrRest { + /** + * Simple and safe wrappers around axios to make RESTful API to mediuroast.io. + * The credential object, passed when this object is created, should include all relevant items + * needed to authenticate a client. This can include appropriate JWT tokens, user identifiers, + * passwords, etc. At a minimum the restServer and an apiKey are needed to connect. + * @constructor + * @classdesc Construct the object to interact with the mediumroast.io application + * @param {Object} credential - contains key items needed to interact with the mediumroast.io application + */ constructor(credential) { this.user = credential.user this.secret = credential.secret @@ -25,9 +29,10 @@ class mrRest { } /** - * Get an object using endpoint only. - * @param {String} endpoint The full URL to the RESTful target - * @param {Returns} result An array starting with a boolean success/failure and resulting data + * @function getObj + * @description Get an object using endpoint only. + * @param {String} endpoint - The full URL to the RESTful target + * @returns {Array} An array starting with a boolean success/failure and resulting data */ async getObj(endpoint) { const myURL = this.restServer + endpoint @@ -49,10 +54,11 @@ class mrRest { } /** - * Post an object using endpoint and a Javascript object. + * @function postObj + * @description Post an object using endpoint and a Javascript object. * @param {String} endpoint The full URL to the RESTful target * @param {Object} obj Data objects for input - * @param {Returns} result An array starting with a boolean success/failure and resulting data + * @returns {Array} An array starting with a boolean success/failure and resulting data */ async postObj(endpoint, obj) { const myURL = this.restServer + endpoint @@ -71,10 +77,11 @@ class mrRest { } /** - * Patch an object using endpoint and a Javascript object. + * @function patchObj + * @description Patch an object using endpoint and a Javascript object. * @param {String} endpoint The full URL to the RESTful target * @param {Object} obj Data objects for input - * @param {Returns} result An array starting with a boolean success/failure and resulting data + * @returns {Array} An array starting with a boolean success/failure and resulting data * @todo This may not be needed for the final implementation, verify with the backend */ async patchObj(endpoint, obj) { @@ -95,10 +102,12 @@ class mrRest { } /** - * Delete an object using endpoint and a Javascript object. + * @function deleteObj + * @description Delete an object using endpoint and a Javascript object. * @param {String} endpoint The full URL to the RESTful target * @param {Object} obj Data objects for input - * @param {Returns} result An array starting with a boolean success/failure and resulting data + * @returns {Array} An array starting with a boolean success/failure and resulting data + * @todo this isn't yet implemented in the backend verification is needed */ async deleteObj(endpoint, obj) { const myURL = this.restServer + endpoint diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..3900a9f --- /dev/null +++ b/src/cli.js @@ -0,0 +1,232 @@ +/** + * A class used to build CLIs for accessing and reporting on mediumroast.io objects + * @author Michael Hay + * @file cli.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.0.0 + */ + +// Import required modules +import program from 'commander' +import ConfigParser from 'configparser' +import Table from 'cli-table' +import Parser from 'json2csv' +import * as XLSX from 'xlsx' +import { Utilities } from './helpers.js' + + +class CLIUtilities { + /** + * A class to create consistent CLI operations for mediumroast.io objects like + * interactions, studies, companies and users. The functions within help with environmental + * settings, command line switches and output formatting. + * @constructor + * @classdesc Construct a CLI object with key parameters + * @param {String} version - the version for the CLI + * @param {String} name - name for the CLI + * @param {String} description - a description for the CLI + * @param {String} objectType - the type of objects the CLI manages + */ + constructor(version, name, description, objectType) { + this.version = version + this.name = name + this.description = description + this.objectType = objectType + this.utils = new Utilities(objectType) + } + + /** + * @function parseCLIArgs + * @description Consistently parse the CLI for options and switches + * @returns {Object} - an object containing all CLI options and switches + */ + parseCLIArgs() { + // Define commandline options + program + .name(this.name) + .version(this.version) + .description(this.description) + + program + // System command line switches + .requiredOption( + '-c --conf_file ', + 'Path to the configuration file', + process.env.HOME + '/.mediumroast/config.ini' + ) + .option( + '-r --rest_server ', + 'The URL of the target mediumroast.io server', + 'http://cherokee.from-ca.com:46767' + ) + .option( + '-a --api_key ', + 'The API key needed to talk to the mediumroast.io server' + ) + .option( + '-u --user ', + 'Your user name for the mediumroast.io server' + ) + .option( + '-s --secret ', + 'Your user secret or password for the mediumroast.io server' + ) + .option( + '-o --output ', + 'Select output type: table, json, xls or csv. xls & csv will save to a file.', + 'table', + 'json', + 'xls', + 'csv' + ) + + // Operational command line switches + .option( + '--find_by_name ', + 'Find an individual Interaction by name' + ) + .option( + '--find_by_id ', + 'Find an individual Interaction by ID' + ) + .option( + '--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' + ) + .option( + '--delete ', + 'Delete an object from the backend by specifying the object\'s id' + ) + .option( + '--report ', + 'Create an MS word document for an object by specifying the object\'s id' + ) + .option( + '--package', + 'An additional switch used with --report to generate a ZIP package that includes the interaction' + ) + + program.parse(process.argv) + return program.opts() + } + + /** + * @function getConfig + * @description Using the confFile argument read, parse and return the contents of a configuration file + * @param {String} confFile - a fully qualified path to the configuration file + * @returns {Object} The object containing the parsed configuration file results + */ + getConfig(confFile) { + const config = new ConfigParser() + config.read(confFile) + return config + } + + + /** + * @function getEnv + * @description With the CLI arguments as the priority create an environmentals object to be used in the CLI + * @param {Object} cliArgs - should contain the results of parseCLIArgs() above + * @param {Object} config - should contain the results of getConfig() above + * @returns {Object} after merging cliArgs and config an Object containing the final environmental settings + */ + 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 + } + + // With the cli options as the priority set up the environment for the cli + cliArgs.rest_server ? env.restServer = cliArgs.rest_server : env.restServer = config.get('DEFAULT', 'rest_server') + cliArgs.api_key ? env.apiKey = cliArgs.api_key : env.apiKey = config.get('DEFAULT', 'api_key') + cliArgs.user ? env.user = cliArgs.user : env.user = config.get('DEFAULT', 'user') + cliArgs.secret ? env.secret = cliArgs.secret : env.secret = config.get('DEFAULT', 'secret') + + // Set up additional parameters from config file + env.workDir = config.get('DEFAULT', 'working_dir') + 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') + + // Return the environmental settings needed for the CLI to operate + return env + } + + + /** + * @function outputCLI + * @description An output router enabling users to pick their output format of choice for a CLI + * @param {String} outputType Type of output to produce/route to: table, json, csv, xls + * @param {Object} results Data objects to be output + * @param {Object} env Environmental variables from the CLI + * @param {String} objType The object type: Interactions, Studies or Companies + */ + outputCLI(outputType, results, env, objType) { + // Emit the output as per the cli options + if (outputType === 'table') { + this.outputTable(results) + } else if (outputType === 'json') { + console.dir(results) + } else if (outputType === 'csv') { + this.outputCSV(results, env) + } else if (outputType === 'xls') { + this.outputXLS(results, env, objType) + } + } + + outputTable(objects) { + let table = new Table({ + head: ['Id', 'Name', 'Description'], + colWidths: [5, 40, 90] + }) + + for (const myObj in objects) { + table.push([ + objects[myObj].id, + objects[myObj].name, + objects[myObj].description + ]) + } + console.log(table.toString()) + } + + outputCSV(objects, env) { + const fileName = 'Mr_' + this.objectType + '.csv' + const myFile = env['outputDir'] + '/' + fileName + const csv = Parser.parse(objects) + this.utils.saveTextFile(myFile, csv) + } + + // TODO add error checking via try catch + outputXLS(objects, env) { + const fileName = 'Mr_' + this.objectType + '.xlsx' + const myFile = env['outputDir'] + '/' + fileName + const mySheet = XLSX.utils.json_to_sheet(objects) + const myWorkbook = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(myWorkbook, mySheet, this.objectType) + XLSX.writeFile(myWorkbook, myFile) + } +} + +export { CLIUtilities } \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js index fe7e537..886a599 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -4,203 +4,34 @@ * @file helpers.js * @copyright 2022 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 + * @version 2.0.0 */ // Import required modules import * as fs from 'fs' -import program from 'commander' -import ConfigParser from 'configparser' -import Table from 'cli-table' -import Parser from 'json2csv' -import * as XLSX from 'xlsx' import zip from 'adm-zip' import AWS from 'aws-sdk' -class CLI { - constructor(version, name, description, objectType) { - this.version = version - this.name = name - this.description = description - this.objectType = objectType - } - - // Parse the cli options - parseCLIArgs() { - // Define commandline options - program - .name(this.name) - .version(this.version) - .description(this.description) - - program - // System command line switches - .requiredOption( - '-c --conf_file ', - 'Path to the configuration file', - process.env.HOME + '/.mediumroast/config.ini' - ) - .option( - '-r --rest_server ', - 'The URL of the target mediumroast.io server', - 'http://cherokee.from-ca.com:46767' - ) - .option( - '-a --api_key ', - 'The API key needed to talk to the mediumroast.io server' - ) - .option( - '-u --user ', - 'Your user name for the mediumroast.io server' - ) - .option( - '-s --secret ', - 'Your user secret or password for the mediumroast.io server' - ) - .option( - '-o --output ', - 'Select output type: table, json, xls or csv. xls & csv will save to a file.', - 'table', - 'json', - 'xls', - 'csv' - ) - - // Operational command line switches - .option( - '--find_by_name ', - 'Find an individual Interaction by name' - ) - .option( - '--find_by_id ', - 'Find an individual Interaction by ID' - ) - .option( - '--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' - ) - .option( - '--delete ', - 'Delete an object from the backend by specifying the object\'s id' - ) - .option( - '--report ', - 'Create an MS word document for an object by specifying the object\'s id' - ) - .option( - '--package', - 'An additional switch used with --report to generate a ZIP package that includes the interaction' - ) - - program.parse(process.argv) - return program.opts() - } - - getConfig(confFile) { - const config = new ConfigParser() - config.read(confFile) - return config - } - - 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 - } - - // With the cli options as the priority set up the environment for the cli - cliArgs.rest_server ? env.restServer = cliArgs.rest_server : env.restServer = config.get('DEFAULT', 'rest_server') - cliArgs.api_key ? env.apiKey = cliArgs.api_key : env.apiKey = config.get('DEFAULT', 'api_key') - cliArgs.user ? env.user = cliArgs.user : env.user = config.get('DEFAULT', 'user') - cliArgs.secret ? env.secret = cliArgs.secret : env.secret = config.get('DEFAULT', 'secret') - - // Set up additional parameters from config file - env.workDir = config.get('DEFAULT', 'working_dir') - 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') - - // Return the environmental settings needed for the CLI to operate - return env - } - - +class Utilities { /** - * An output router enabling users to pick their output format of choice for a CLI - * @param {String} outputType Type of output to produce/route to: table, json, csv, xls - * @param {Object} results Data objects to be output - * @param {Object} env Environmental variables from the CLI - * @param {String} objType The object type: Interactions, Studies or Companies + * A class to enable consistent functionality for basic operations like writing files, + * downloading from S3, reading files, creating ZIP archives, etc. + * @constructor + * @classdesc Largely reserved for future use a basic constructor to create the object + * @param {String} objectType - The type of object constructing this object */ - outputCLI(outputType, results, env, objType) { - // Emit the output as per the cli options - if (outputType === 'table') { - this.outputTable(results) - } else if (outputType === 'json') { - console.dir(results) - } else if (outputType === 'csv') { - this.outputCSV(results, env) - } else if (outputType === 'xls') { - this.outputXLS(results, env, objType) - } + constructor(objectType) { + this.objectType = objectType ? objectType : null } - outputTable(objects) { - let table = new Table({ - head: ['Id', 'Name', 'Description'], - colWidths: [5, 40, 90] - }) - - for (const myObj in objects) { - table.push([ - objects[myObj].id, - objects[myObj].name, - objects[myObj].description - ]) - } - console.log(table.toString()) - } - - outputCSV(objects, env) { - const fileName = 'Mr_' + this.objectType + '.csv' - const myFile = env['outputDir'] + '/' + fileName - // const parser = new Parser() - const csv = Parser.parse(objects) - console.log(csv) - this.saveTextFile(myFile, csv) - } - - // TODO add error checking via try catch - outputXLS(objects, env) { - const fileName = 'Mr_' + this.objectType + '.xlsx' - const myFile = env['outputDir'] + '/' + fileName - const mySheet = XLSX.utils.json_to_sheet(objects) - const myWorkbook = XLSX.utils.book_new() - XLSX.utils.book_append_sheet(myWorkbook, mySheet, this.objectType) - XLSX.writeFile(myWorkbook, myFile) - } - - + /** + * @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} content - 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, content) { fs.writeFileSync(fileName, content, err => { if (err) { @@ -211,6 +42,12 @@ class CLI { }) } + /** + * @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') @@ -220,21 +57,31 @@ class CLI { } } - // simple function for safe directory creation - safeMakedir(name) { + /** + * @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(name)) { - fs.mkdirSync(name, { recursive: true }) - return [true, 'Created directory [' + name + ']', null] + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }) + return [true, 'Created directory [' + dirName + ']', null] } else { - return [true, 'Directory [' + name + '] exists did not create.', null] + return [true, 'Directory [' + dirName + '] exists did not create.', null] } } catch (err) { - return [false, 'Did not create directory [' + name + '] because: ' + err, null] + return [false, 'Did not create directory [' + dirName + '] because: ' + err, null] } } - // Recursively remove a directory + /** + * @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.rmdirSync(dirName, {recursive: true}) @@ -245,6 +92,13 @@ class CLI { } // create a ZIP package + /** + * @function createZIPArchive + * @description Create a ZIP package from a source directory + * @param {String} outputFile - the name, including the full path name, of the target ZIP package + * @param {Sting} 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(outputFile, sourceDirectory) { try { const zipPackage = new zip() @@ -256,7 +110,12 @@ class CLI { } } - // Extract a ZIP package + /** + * @function extractZIPArchive + * @description Extract objects from a ZIP package into a target directory + * @param {String} inputFile - the ZIP file name, including the full path, to be extracted + * @param {String} targetDirectory - the location for the ZIP package to be extracted to + */ async extractZIPArchive(inputFile, targetDirectory) { try { const zipPackage = new zip(inputFile) @@ -267,7 +126,14 @@ class CLI { } } - // Download the objects + /** + * @function s3DownloadObjs + * @description From an S3 bucket download the document associated to each interaction + * @param {Array} interactions - an array of interaction objects + * @param {Object} env - the environmental settings to use for accessing the S3 endpoint + * @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 + */ async s3DownloadObjs (interactions, env, targetDirectory) { const s3Ctl = new AWS.S3({ accessKeyId: env.s3User , @@ -289,4 +155,4 @@ class CLI { } -export { CLI } \ No newline at end of file +export { Utilities } \ No newline at end of file diff --git a/src/report/common.js b/src/report/common.js index e0098ce..2723a5d 100644 --- a/src/report/common.js +++ b/src/report/common.js @@ -15,7 +15,22 @@ import boxPlot from 'box-plot' // TODO Change class names to: GenericUtilities, DOCXUtilities and HTMLUtilities // TODO rankTags belongs to GenericUtilities -class Utilities { +class DOCXUtilities { + /** + * To make machine authoring of a Microsoft DOCX file consistent and easier this class has been + * developed. Key functions are available that better describe the intent of the operation + * by name which makes it simpler author a document instead of sweating the details. + * Further through trial and error the idiosyncrasies of the imported docx module + * have been worked out so that the developer doesn't have to accidently find out and struggle + * with document generation. + * @constructor + * @classdesc Core utilities for generating elements in a Microsoft word DOCX file + * @param {String} font + * @param {Float} fontSize + * @param {Float} textFontSize + * @param {String} textFontColor + * @todo when we get to HTML report generation for the front end we will rename this class and create a new one for HTML + */ constructor (font, fontSize, textFontSize, textFontColor) { this.font = font ? font : 'Avenir Next' this.size = fontSize ? fontSize : 11 @@ -233,7 +248,13 @@ class Utilities { } } - // Create a bullet for a bit of prose + /** + * @function makeBullet + * @description Create a bullet for a bit of prose + * @param {String} text - text/prose for the bullet + * @param {Integer} level - the level of nesting for the bullet + * @returns {Object} new docx paragraph object as a bullet + */ makeBullet(text, level=0) { return new docx.Paragraph({ text: text, @@ -244,7 +265,15 @@ class Utilities { }) } - // For a section of prose create a paragraph + /** + * @function makeParagraph + * @description For a section of prose create a paragraph + * @param {String} paragraph - text/prose for the paragraph + * @param {Integer} size - font size for the paragrah + * @param {Boolean} bold - a boolean value for determining if the text should be bolded + * @param {Integer} spaceAfter - an integer 1 or 0 to determine if there should be space after this element + * @returns {Object} a docx paragraph object + */ makeParagraph (paragraph, size, bold, spaceAfter) { return new docx.Paragraph({ children: [ @@ -259,7 +288,14 @@ class Utilities { }) } - // Create a text run with or without space after + // + /** + * @function makeTextrun + * @description Create a text run with or without space after + * @param {String} text - text/prose for the textrun + * @param {Integer} spaceAfter - an integer 1 or 0 to determine if there should be space after this element + * @returns {Object} a docx textrun object + */ makeTextrun(text, spaceAfter=false) { const myFontSize = 16 if (spaceAfter) { @@ -278,7 +314,11 @@ class Utilities { } } - // Create a page break + /** + * @function pageBreak + * @description Create a page break + * @returns {Object} a docx paragraph object with a PageBreak + */ pageBreak() { return new docx.Paragraph({ children: [ @@ -287,7 +327,12 @@ class Utilities { }) } - // Create a text of heading style 1 + /** + * @function makeHeading1 + * @description Create a text of heading style 1 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ makeHeading1(text) { return new docx.Paragraph({ text: text, @@ -295,7 +340,12 @@ class Utilities { }) } - // Create a text of heading style 2 + /** + * @function makeHeading2 + * @description Create a text of heading style 2 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ makeHeading2(text) { return new docx.Paragraph({ text: text, @@ -303,7 +353,13 @@ class Utilities { }) } - // Create a text of heading style 2 + + /** + * @function makeHeading3 + * @description Create a text of heading style 3 + * @param {String} text - text/prose for the function + * @returns {Object} a new paragraph as a heading + */ makeHeading3(text) { return new docx.Paragraph({ text: text, @@ -311,7 +367,13 @@ class Utilities { }) } - // Create an external hyperlink + /** + * @function makeExternalHyperLink + * @description Create an external hyperlink + * @param {String} text - text/prose for the function + * @param {String} link - the URL for the hyperlink + * @returns {Object} a new docx ExternalHyperlink object + */ makeExternalHyperLink(text, link) { return new docx.ExternalHyperlink({ children: [ @@ -326,7 +388,13 @@ class Utilities { }) } - // Create an internal hyperlink + /** + * @function makeInternalHyperLink + * @description Create an external hyperlink + * @param {String} text - text/prose for the function + * @param {String} link - the URL for the hyperlink within the document + * @returns {Object} a new docx InternalHyperlink object + */ makeInternalHyperLink(text, link) { return new docx.InternalHyperlink({ children: [ @@ -341,8 +409,14 @@ class Utilities { }) } - // Create a bookmark needed to create an internal hyperlink - // TODO at some point test this + /** + * @function makeBookmark + * @description Create a target within a document to link to with an internal hyperlink + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark + * @todo test and revise this function as it may need to be a textrun which can be embedded in something else + */ makeBookmark(text, ident) { return new docx.Paragraph({ children: [ @@ -356,7 +430,14 @@ class Utilities { }) } - // Create a bookmark needed to create an internal hyperlink + /** + * @function makeHeadingBookmark1 + * @description Create a target within a document to link to with an internal hyperlink of heading 1 + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark at the heading level 1 + * @todo could we generalize this function and make the heading level a parameter in the future? + */ makeHeadingBookmark1(text, ident) { return new docx.Paragraph({ heading: docx.HeadingLevel.HEADING_1, @@ -371,7 +452,13 @@ class Utilities { }) } - // Create a bookmark needed to create an internal hyperlink + /** + * @function makeHeadingBookmark2 + * @description Create a target within a document to link to with an internal hyperlink of heading 2 + * @param {String} text - text/prose for the function + * @param {String} ident - the unique name of the bookmark + * @returns {Object} a new docx paragraph object with a bookmark at the heading level 2 + */ makeHeadingBookmark2(text, ident) { return new docx.Paragraph({ heading: docx.HeadingLevel.HEADING_2, @@ -388,7 +475,13 @@ class Utilities { - // Basic table row to produce a name/value pair + /** + * @function basicRow + * @description Basic table row to produce a name/value pair table with 2 columns + * @param {String} name - text/prose for the cell + * @param {String} data - text/prose for the cell + * @returns {Object} a new docx TableRow object + */ basicRow (name, data) { // return the row return new docx.TableRow({ @@ -411,7 +504,13 @@ class Utilities { }) } - // Create rows for object ids and object descriptions + /** + * @function descriptionRow + * @description Description table row to produce a name/value pair table with 2 columns + * @param {String} id - text/prose for the cell + * @param {String} description - text/prose for the cell + * @returns {Object} a new docx TableRow object + */ descriptionRow(id, description, bold=false) { // return the row return new docx.TableRow({ @@ -434,7 +533,14 @@ class Utilities { }) } - // Create the rows with URLs/links + /** + * @function urlRow + * @description Hyperlink table row to produce a name/value pair table with 2 columns and an external hyperlink + * @param {String} category - text/prose for the first column + * @param {String} name - text/prose for the hyperlink in the second column + * @param {String} link - the URL for the hyperlink + * @returns {Object} a new docx TableRow object with an external hyperlink + */ urlRow(category, name, link) { // define the link to the target URL const myUrl = new docx.ExternalHyperlink({ @@ -470,6 +576,15 @@ class Utilities { }) } + /** + * @function basicTopicRow + * @description Create a 3 column row for displaying topics which are the results of term extraction + * @param {String} theme - text/prose for the theme in col 1 + * @param {Float} score - the numerical score for the term in col 2 + * @param {String} rank - a textual description of the relative priority for the term in col 3 + * @param {Boolean} bold - whether or not to make the text/prose bold typically used for header row + * @returns {Object} a new 3 column docx TableRow object + */ basicTopicRow (theme, score, rank, bold) { const myFontSize = 16 // return the row @@ -503,7 +618,16 @@ class Utilities { }) } - // Build a comparisons row + /** + * @function basicComparisonRow + * @description Create a 4 column row for displaying comparisons which are the results of similarity comparisons + * @param {String} company - text/prose for the company in col 1 + * @param {String} role - the role of the company in col 2 + * @param {Float} score - the numerical score for the term in col 3 + * @param {String} rank - a textual description of the relative priority for the company in col 4 + * @param {Boolean} bold - whether or not to make the text/prose bold typically used for header row + * @returns {Object} a new 4 column docx TableRow object + */ basicComparisonRow (company, role, score, rank, bold) { const myFontSize = 16 // return the row @@ -547,7 +671,14 @@ class Utilities { - // Write the report to storage + /** + * @async + * @function writeReport + * @description safely write a DOCX report to a desired location + * @param {Object} docObj - a complete and error free document object that is ready to be saved + * @param {String} fileName - the file name for the DOCX object + * @returns {Array} an array containing if the save operation succeeded, the message, and null + */ async writeReport (docObj, fileName) { try { await docx.Packer.toBuffer(docObj).then((buffer) => { @@ -559,7 +690,12 @@ class Utilities { } } - // Rank supplied topics and return an object that can be rendered + /** + * @function rankTags + * @description Rank supplied topics and return an object that can be rendered + * @param {Object} tags - the tags from the source object to be ranked + * @returns {Object} the final tags which now have ranking and are suitable for a basicTopicRow + */ rankTags (tags) { const ranges = boxPlot(Object.values(tags)) let finalTags = {} @@ -586,7 +722,12 @@ class Utilities { return finalTags } - // Create a table for topics + /** + * @function topicTable + * @description A higher level function that calls basicTopicRow to create a complete table + * @param {Object} topics - the result of rankTags + * @returns {Object} a complete docx table that includes topics + */ topicTable(topics) { let myRows = [this.basicTopicRow('Keywords', 'Score', 'Rank', true)] for (const topic in topics) { @@ -604,6 +745,20 @@ class Utilities { return myTable } + + // Create an introductory section + /** + * @function makeIntro + * @description Creates a complete document with a heading of level 1 + * @param {String} introText - text/prose for the introduction + * @returns {Object} a complete introduction with heading level 1 and a paragraph + */ + makeIntro (introText) { + return [ + this.makeHeading1('Introduction'), + this.makeParagraph(introText) + ] + } } -export default Utilities \ No newline at end of file +export default DOCXUtilities \ No newline at end of file diff --git a/src/report/companies.js b/src/report/companies.js index f1099d1..19c4b7e 100644 --- a/src/report/companies.js +++ b/src/report/companies.js @@ -1,16 +1,34 @@ +/** + * Two classes to create sections and documents for company objects in mediumroast.io + * @author Michael Hay + * @file companies.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.0.0 + */ + // Import required modules import docx from 'docx' import boxPlot from 'box-plot' -import Utilities from './common.js' +import DOCXUtilities from './common.js' import { InteractionSection } from './interactions.js' class CompanySection { + /** + * A high level class to create sections for a Company report using either + * Microsoft DOCX format or eventually HTML format. Right now the only available + * implementation is for the DOCX format. These sections are designed to be consumed + * by a wrapping document which could be for any one of the mediumroast objects. + * @constructor + * @classdesc To operate this class the constructor should be passed a single company object. + * @param {Object} company - The company object to generate the section(s) for + */ constructor(company) { this.company = company this.company.stock_symbol === 'Unknown' && this.company.cik === 'Unknown' ? this.companyType = 'Private' : this.companyType = 'Public' - this.util = new Utilities() + this.util = new DOCXUtilities() } // Create a URL on Google maps to search for the address @@ -79,7 +97,12 @@ class CompanySection { } } - makeFirmographics() { + /** + * @function makeFirmographicsDOCX + * @description Create a table containing key information for the company in question + * @returns {Object} A docx table is return to the caller + */ + makeFirmographicsDOCX() { const noInteractions = String(Object.keys(this.company.linked_interactions).length) const noStudies = String(Object.keys(this.company.linked_studies).length) const myTable = new docx.Table({ @@ -157,7 +180,13 @@ class CompanySection { return [finalComparisons, rankPicker] } - makeComparison(comparisons) { + /** + * @function makeComparisonDOCX + * @description Generate the comparisons section for the document from the company in question + * @param {Object} comparisons - the object containing the comparisons for the company in question + * @returns {Array} An array containing an introduction to this section and the table with the comparisons + */ + makeComparisonDOCX(comparisons) { // Transform the comparisons into something that is usable for display const [myComparison, picks] = this.rankComparisons(comparisons) @@ -204,7 +233,19 @@ class CompanySection { } } + class CompanyStandalone { + /** + * A high level class to create a complete document for a Company report using either + * Microsoft DOCX format or eventually HTML format. Right now the only available + * implementation is for the DOCX format. + * @constructor + * @classdesc Create a full and standlaone report document for a company + * @param {Object} company - the company object to be reported on + * @param {Array} interactions - the interactions associated to the company + * @param {String} creator - the author of the report + * @param {*} authorCompany - the company of the report author + */ constructor(company, interactions, creator, authorCompany) { this.objectType = 'Company' this.creator = creator @@ -218,23 +259,21 @@ class CompanyStandalone { ' If this report document is produced as a package, instead of standalone, then the' + ' hyperlinks are active and will link to documents on the local folder after the' + ' package is opened.' - this.util = new Utilities() + this.util = new DOCXUtilities() this.topics = this.util.rankTags(this.company.topics) this.comparison = company.comparison, this.noInteractions = String(Object.keys(this.company.linked_interactions).length) } - // TODO Move to common.js - makeIntro () { - const myIntro = [ - this.util.makeHeading1('Introduction'), - this.util.makeParagraph(this.introduction) - ] - return myIntro - } - - - async makeDocx(fileName, isPackage) { + /** + * @async + * @function makeDocx + * @description Generate and save a DOCX report for a Company object + * @param {String} fileName - Full path to the file name, if no file name is supplied a default is assumed + * @param {Boolean} isPackage - When set to true links are set up for connecting to interaction documents + * @returns {Array} The result of the writeReport function that is an Array + */ + async makeDOCX(fileName, isPackage) { // If fileName isn't specified create a default fileName = fileName ? fileName : process.env.HOME + '/Documents/' + this.company.name.replace(/ /g,"_") + '.docx' @@ -251,13 +290,13 @@ class CompanyStandalone { // Set up the default options for the document const myDocument = [].concat( - this.makeIntro(), + this.util.makeIntro(this.introduction), [ this.util.makeHeading1('Company Detail'), - companySection.makeFirmographics(), + companySection.makeFirmographicsDOCX(), this.util.makeHeading1('Comparison') ], - companySection.makeComparison(this.comparison), + companySection.makeComparisonDOCX(this.comparison), [ this.util.makeHeading1('Topics'), this.util.makeParagraph( 'The following topics were automatically generated from all ' + @@ -267,11 +306,11 @@ class CompanyStandalone { this.util.topicTable(this.topics), this.util.makeHeadingBookmark1('Interaction Summaries', 'interaction_summaries') ], - ...interactionSection.makeDescriptions(), + ...interactionSection.makeDescriptionsDOCX(), [ this.util.pageBreak(), this.util.makeHeading1('References') ], - ...interactionSection.makeReferences(isPackage) + ...interactionSection.makeReferencesDOCX(isPackage) ) // Construct the document diff --git a/src/report/company.js b/src/report/company.js deleted file mode 100644 index fb9242c..0000000 --- a/src/report/company.js +++ /dev/null @@ -1,271 +0,0 @@ -// Import required modules -import docx from 'docx' -import References from './interactions.js' -import KeyThemes from './themes.js' - -class Firmographics { - // Consider a switch between HTML and DOCX - // NOTE This may not be needed for the HTML version more thinking needed - constructor(company, interactions, protocol, themes, quotes) { - // Decode the regions - const regions = { - AMER: 'Americas', - EMEA: 'Europe, Middle East and Africa', - APAC: 'Asia Pacific and Japan' - } - - // Set the company Type - company[0].stockSymbol === 'Unknown' && company[0].cik === 'Unknown' ? - this.companyType = 'Private' : - this.companyType = 'Public' - - this.company = company[0] - this.region = regions[company[0].region] - this.font = 'Avenir Next' // We need to pass this in from the config file - this.fontSize = 10 // We need to pass this in from the config file - this.fontFactor = 1.5 - this.interactions = interactions - this.protocol = protocol ? protocol : false - this.themes = themes ? themes : false - this.quotes = quotes ? quotes : false - this.companyDoc = this.doc() - } - - // Define the CIK and link it to an EDGAR search if available - stockSymbolRow () { - if (this.company.stockSymbol === 'Unknown') { - return this.basicRow('Stock Symbol', this.company.stockSymbol) - } else { - const baseURL = 'https://www.bing.com/search?q=' - return this.urlRow('Stock Symbol', this.company.stockSymbol, baseURL + this.company.stockSymbol) - } - } - - // Define the CIK and link it to an EDGAR search if available - cikRow () { - if (this.company.cik === 'Unknown') { - return this.basicRow('CIK', this.company.cik) - } else { - const baseURL = 'https://www.sec.gov/edgar/search/#/ciks=' - return this.urlRow('CIK', this.company.cik, baseURL + this.company.cik) - } - } - - // Create the website row - urlRow(category, name, link) { - // define the link to the target URL - const myUrl = new docx.ExternalHyperlink({ - children: [ - new docx.TextRun({ - text: name, - style: 'Hyperlink', - font: this.font, - size: this.fontFactor * this.fontSize - }) - ], - link: link - }) - - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(category, this.fontFactor * this.fontSize, true)] - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [new docx.Paragraph({children:[myUrl]})] - }) - ] - }) - } - - // Basic row to produce a name/value pair - basicRow (name, data) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(name, this.fontFactor * this.fontSize, true)] - }), - new docx.TableCell({ - width: { - size: 80, - type: docx.WidthType.PERCENTAGE - }, - children: [this.makeParagraph(data, this.fontFactor * this.fontSize)] - }) - ] - }) - } - - // Create the table for the doc - docTable() { - // Define the address string - const addressBits = [ - this.company.streetAddress, - this.company.city, - this.company.stateProvince, - this.company.zipPostal, - this.company.country - ] - const addressBaseUrl = 'https://www.google.com/maps/place/' - let addressSearch = "" - for (const element in addressBits) { - let tmpString = addressBits[element] - tmpString = tmpString.replace(' ', '+') - addressSearch+='+' + tmpString - } - const addressUrl = addressBaseUrl + encodeURIComponent(addressSearch) - const addressString = addressBits[0] + ', ' + - addressBits[1] + ', ' + addressBits[2] + ' ' + addressBits[3] + ', ' + - addressBits[4] - - const patentString = this.company.companyName + ' Patent Search' - const patentURL = 'https://patents.google.com/?assignee=' + this.company.companyName - - const newsString = this.company.companyName + ' Company News' - const newsURL = 'https://news.google.com/search?q=' + this.company.companyName - - // define the table with firmographics - const myTable = new docx.Table({ - columnWidths: [20, 80], - rows: [ - this.basicRow('Name', this.company.companyName), - this.basicRow('Description', this.company.description), - this.urlRow('Website', this.company.url, this.company.url), - this.basicRow('Role', this.company.role), - this.basicRow('Industry', this.company.industry), - this.urlRow('Patents', patentString, patentURL), - this.urlRow('News', newsString, newsURL), - this.urlRow('Location', addressString, addressUrl), - this.basicRow('Region', this.region), - this.basicRow('Phone', this.company.phone), - this.basicRow('Type', this.companyType), - this.stockSymbolRow(), - this.cikRow(), - this.basicRow('No. Interactions', String(this.company.totalInteractions)), - this.basicRow('No. Studies', String(this.company.totalStudies)), - // TODO Add Rows and maybe URLs for current interaction and study - // NOTE this requires the URL for the mr_backend - ], - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - - return myTable - } - - // For a section of prose create a paragraph - makeParagraph (paragraph, size, bold) { - return new docx.Paragraph({ - children: [ - new docx.TextRun({ - text: paragraph, - font: this.font, - size: size ? size : 20, - bold: bold ? bold : false - }) - ] - }) - } - - // Create a title of heading style 1 - makeTitle(title) { - return new docx.Paragraph({ - text: title, - heading: docx.HeadingLevel.HEADING_1 - }) - } - - makeActions () { - // TODO inherit bullet and numbering styles in doc_company - let actionArray = [] - for (const action in this.company.document.Action) { - if (action === 'text') { continue } - const protoAction = this.company.document.Action[action] - const [actionText, actionStatus] = protoAction.split('|') - // console.log(actionText, actionStatus) - actionArray.push( - new docx.Paragraph({ - text: actionText, - numbering: { - reference: 'number-styles', - level: 0 - } - }), - new docx.Paragraph({ - text: actionStatus, - numbering: { - reference: 'number-styles', - level: 1 - } - }) - ) - } - return actionArray - } - - // Create a page break - pageBreak() { - return new docx.Paragraph({ - children: [ - new docx.PageBreak() - ] - }) - } - - // Generate a page with all company firmographics - doc() { - - // Create the references from supplied interactions - const refCtl = new References( - this.interactions, - this.company.companyName, - 'company', - this.protocol) - const myReferences = refCtl.makeDocx() - - // Create summary themes from supplied themes if needed - let myThemes = [] - if (this.themes) { - const themeCtl = new KeyThemes('summary', this.themes, this.quotes, this.interactions) - myThemes = themeCtl.makeDocx() - } - - return [].concat( - [this.makeTitle('Introduction'), // Intro title - this.makeParagraph(this.company.document.Introduction), // Introduction paragraph - this.makeTitle('Purpose'), // Intro title - this.makeParagraph(this.company.document.Purpose), // Purpose paragraph - this.makeTitle('Actions'), // Actions title - this.makeParagraph(this.company.document.Action.text)], - ...this.makeActions(), - [this.pageBreak(), // Add a page break - this.makeTitle('Firmographics'), // Firmographics title - this.docTable(), // Table containing firmographics - ...myThemes, // Section for the summary theme if available - this.pageBreak(), - this.makeTitle('References')], - ...myReferences - ) - } - - - -} - -export default Firmographics \ No newline at end of file diff --git a/src/report/interactions.js b/src/report/interactions.js index cbd6036..1e52afb 100644 --- a/src/report/interactions.js +++ b/src/report/interactions.js @@ -1,13 +1,31 @@ +/** + * Two classes to create sections and documents for interaction objects in mediumroast.io + * @author Michael Hay + * @file interactions.js + * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @version 1.0.0 + */ + // Import required modules import docx from 'docx' -import boxPlot from 'box-plot' - -import Utilities from './common.js' +import DOCXUtilities from './common.js' import { CompanySection } from './companies.js' class InteractionSection { - constructor(interactions, objectName, objectType, characterLimit = 1000) { + /** + * A high level class to create sections for an Interaction report using either + * Microsoft DOCX format or eventually HTML format. Right now the only available + * implementation is for the DOCX format. These sections are designed to be consumed + * by a wrapping document which could be for any one of the mediumroast objects. + * @constructor + * @classdesc Construct the InteractionSection object + * @param {Array} interactions - a complete array of interactions ranging from 1:N + * @param {String} objectName - the name of the object calling this class + * @param {String} objectType - the type of object calling this class + */ + constructor(interactions, objectName, objectType) { // NOTE creation of a ZIP package is something we likely need some workspace for // since the documents should be downloaded and then archived. Therefore, @@ -15,160 +33,32 @@ class InteractionSection { // we will need some server side logic to make this happen. this.interactions = interactions - this.characterLimit = characterLimit - // TODO anything that is commented out can likely be removed - // this.introduction = 'The mediumroast.io system has automatically generated this section.' + - // ' It includes key metadata from each interaction associated to the object ' + objectName + - // '. If this report document is produced as a package, instead of standalone, then the' + - // ' hyperlinks are active and will link to documents on the local folder after the' + - // ' package is opened.' this.objectName = objectName this.objectType = objectType - // TODO anything that is commented out can likely be removed - // this.font = 'Avenir Next' // We need to pass this in from the config file this.fontSize = 10 // We need to pass this in from the config file - // this.protocol = protocol - // this.protoDoc = this.createRefs() - this.util = new Utilities() + this.util = new DOCXUtilities() } - // Create the entire section as a proto document to be fed to a format like docx, ..., html. - // createRefs() { - // let protoDoc = { - // intro: this.introduction, - // references: {} - // } - // for (const item in this.interactions) { - // protoDoc.references[this.interactions[item].interactionName] = this.createRef(this.interactions[item]) - // } - // return protoDoc - // } - - // // Create an individual reference from an interaction - // createRef(interaction, dateKey = 'date', timeKey = 'time', httpType = 'http') { - // // NOTE need to think about the URL and how to properly unpack it - // // example, swap to from X to http and then preserve it as a - // // part of the protoDoc - - // // Decode the date - // const myDate = interaction[dateKey] - // const [year, month, day] = [myDate.substring(0, 4), myDate.substring(4, 6), myDate.substring(6, 8)] - - // // Decode the time - // const myTime = interaction[timeKey] - // const [hour, min] = [myTime.substr(0, 2), myTime.substr(2, 4)] - - // // Detect the repository type and replace it with http - // // NOTE This is setup to create a local package with source links in a local working directory - // const repoType = interaction.url.split('://')[0] - // let myURL = interaction.url.split('://').pop() - // myURL = this.protocol + myURL.split('/').pop() - - // // Create the reference - // let reference = { - // type: interaction.interactionType, // TODO There is a bug here in the ingestion - // abstract: interaction.abstract.substr(0, this.characterLimit) + '...', - // date: year + '-' + month + '-' + day, - // time: hour + ':' + min, - // url: myURL, - // repo: repoType, - // guid: interaction.GUID - // } - // // Set the object type and name - // reference[this.objectType] = this.objectName - - // return reference - // } - - // // Create a paragraph - // makeParagraph(paragraph, size, bold) { - // return new docx.Paragraph({ - // children: [ - // new docx.TextRun({ - // text: paragraph, - // font: this.font, - // size: size ? size : 20, - // bold: bold ? bold : false, - // }) - // ] - // }) - // } - - // // Create a title of heading style 2 - // makeTitle(title, ident) { - // return new docx.Paragraph({ - // text: text, - // heading: docx.HeadingLevel.HEADING_2 - // }) - // } - - // // Create a text run - // makeTextrun(text) { - // return new docx.TextRun({ - // text: text, - // font: this.font, - // size: 1.5 * this.fontSize, - // }) - // } - - // makeURL(name, link) { - // return new docx.ExternalHyperlink({ - // children: [ - // new docx.TextRun({ - // text: name, - // style: 'Hyperlink', - // font: this.font, - // size: 1.5 * this.fontSize - // }) - // ], - // link: link - // }) - // } - - // // Return the proto document as a docx formatted section - // makeDocx() { - // const excerptAnchor = this.util.makeInternalHyperLink('Summary Excerpts', 'summary_excerpts') - // let finaldoc = [this.makeParagraph(this.protoDoc.intro)] - - // for (const myReference in this.protoDoc.references) { - // // String(this.protoDoc.references[myReference].guid) - // finaldoc.push(this.util.makeBookmark2(myReference, String(this.protoDoc.references[myReference].guid).substring(0, 40))) - // finaldoc.push(this.makeParagraph( - // this.protoDoc.references[myReference].abstract, - // 1.5 * this.fontSize)) - // const permaLink = this.makeURL( - // 'Document link', - // this.protoDoc.references[myReference].url) - - // finaldoc.push( - // new docx.Paragraph({ - // children: [ - // this.makeTextrun('[ '), - // permaLink, - // this.makeTextrun(' | Date: ' + this.protoDoc.references[myReference].date + ' | '), - // this.makeTextrun('Time: ' + this.protoDoc.references[myReference].time + ' | '), - // this.makeTextrun('Type: ' + this.protoDoc.references[myReference].type + ' | '), - // excerptAnchor, - // this.makeTextrun(' ]'), - // ] - // }) - // ) - // } - // return finaldoc - // } - - // Generate the descriptions for interactions - makeDescriptions () { - // TODO create bookmark with the right kind of heading + /** + * @function makeDescriptionsDOCX + * @description Make the descriptions for interactions in the DOCX format + * @returns {Array} An array containing a section description and a table of interaction descriptions + */ + makeDescriptionsDOCX () { + // Set the number of interactions for use later const noInteractions = this.interactions.length + + // Create the header row for the descriptions let myRows = [this.util.descriptionRow('Id', 'Description', true)] - // TODO ids should be hyperlinks to the actual interaction which is interaction_ + // Loop over the interactions and pull out the interaction ids and descriptions for (const interaction in this.interactions) { myRows.push(this.util.descriptionRow( + // Create the internal hyperlink for the interaction reference this.util.makeInternalHyperLink( this.interactions[interaction].id, 'interaction_' + String(this.interactions[interaction].id) ), + // Pull in the description this.interactions[interaction].description ) ) @@ -184,6 +74,7 @@ class InteractionSection { } }) + // Return the results as an array return [ this.util.makeParagraph( 'This section contains descriptions for the ' + noInteractions + ' interactions associated to the ' + @@ -194,15 +85,20 @@ class InteractionSection { ] } - // Create the references for calling programs - makeReferences(isPackage, independent=false) { + /** + * @function makeReferencesDOCX + * @description Create the references for calling programs in the DOCX format + * @param {Boolean} isPackage - When set to true links are set up for connecting to interaction documents + * @returns {Array} An array containing a section description and a table of interaction references + */ + makeReferencesDOCX(isPackage) { // Link this back to the descriptions section const descriptionsLink = this.util.makeInternalHyperLink( 'Back to Interaction Summaries', 'interaction_summaries' ) - // Create the array for the references with the introduction + // Create the array for the references starting with the introduction let references = [ this.util.makeParagraph( 'The mediumroast.io system has automatically generated this section.' + @@ -213,7 +109,9 @@ class InteractionSection { ) ] + // Loop over all interactions for (const interaction in this.interactions) { + // Create the link to the underlying interaction document const objWithPath = this.interactions[interaction].url.split('://').pop() const myObj = objWithPath.split('/').pop() @@ -225,7 +123,7 @@ class InteractionSection { // Depending upon if this is a package or not create the metadata strip with/without document link let metadataStrip = null if(isPackage) { - // Package version of the strip + // isPackage version of the strip metadataStrip = new docx.Paragraph({ spacing: { before: 100, @@ -243,7 +141,7 @@ class InteractionSection { ] }) } else { - // Non package version of the strip + // Non isPackage version of the strip metadataStrip = new docx.Paragraph({ spacing: { before: 100, @@ -289,11 +187,20 @@ class InteractionSection { // Return the built up references return references } - - } class InteractionStandalone { + /** + * A high level class to create a complete document for an Interaction report using either + * Microsoft DOCX format or eventually HTML format. Right now the only available + * implementation is for the DOCX format. + * @constructor + * @classdesc Create a full and standlaone report document for an interaction + * @param {Object} interaction - The interaction in question to process + * @param {Object} company - The company associated to the interaction + * @param {String} creator - A string defining the creator for this document + * @param {String} authorCompany - A string containing the company who authored the document + */ constructor(interaction, company, creator, authorCompany) { this.creator = creator this.authorCompany = authorCompany @@ -307,49 +214,11 @@ class InteractionStandalone { ' hyperlinks are active and will link to documents on the local folder after the' + ' package is opened.' this.abstract = interaction.abstract - this.util = new Utilities() - // TODO test rankTags in common.js - this.topics = this.rankTags(this.interaction.topics) - // this.topics = this.util.rankTags(this.interaction.topics) + this.util = new DOCXUtilities() + this.topics = this.util.rankTags(this.interaction.topics) } - // TODO remove this and rely on the one in utils - rankTags (tags) { - const ranges = boxPlot(Object.values(this.interaction.topics)) - let finalTags = {} - for (const tag in tags) { - // Rank the tag score using the ranges derived from box plots - // if > Q3 then the ranking is high - // if in between Q2 and Q3 then the ranking is medium - // if < Q3 then the ranking is low - let rank = null - if (tags[tag] > ranges.upperQuartile) { - rank = 'High' - } else if (tags[tag] < ranges.lowerQuartile) { - rank = 'Low' - } else if (ranges.lowerQuartile <= tags[tag] <= ranges.upperQuartile) { - rank = 'Medium' - } - - finalTags[tag] = { - score: tags[tag], // Math.round(tags[tag]), - rank: rank - } - - } - return finalTags - } - - // TODO consider moving this to common - makeIntro () { - const myIntro = [ - this.util.makeHeading1('Introduction'), - this.util.makeParagraph(this.introduction) - ] - return myIntro - } - - metadataTable (isPackage) { + metadataTableDOCX (isPackage) { // Switch the name row if depending upon if this is a package or not const objWithPath = this.interaction.url.split('://').pop() const myObj = objWithPath.split('/').pop() @@ -358,7 +227,6 @@ class InteractionStandalone { nameRow = this.util.urlRow('Interaction Name', this.interaction.name, './interactions/' + myObj) : nameRow = this.util.basicRow('Interaction Name', this.interaction.name) - const myTable = new docx.Table({ columnWidths: [20, 80], rows: [ @@ -376,8 +244,15 @@ class InteractionStandalone { return myTable } - // Create the document - async makeDocx(fileName, isPackage) { + /** + * @async + * @function makeDOCX + * @description Create the DOCX document for a single interaction which includes a company section + * @param {String} fileName - Full path to the file name, if no file name is supplied a default is assumed + * @param {Boolean} isPackage - When set to true links are set up for connecting to interaction documents + * @returns {Array} The result of the writeReport function that is an Array + */ + async makeDOCX(fileName, isPackage) { // If fileName isn't specified create a default fileName = fileName ? fileName : process.env.HOME + '/Documents/' + this.interaction.name.replace(/ /g,"_") + '.docx' @@ -386,16 +261,16 @@ class InteractionStandalone { // Set up the default options for the document const myDocument = [].concat( - this.makeIntro(), + this.util.makeIntro(this.introduction), [ this.util.makeHeading1('Interaction Detail'), - this.metadataTable(isPackage), + this.metadataTableDOCX(isPackage), this.util.makeHeading1('Topics'), this.util.topicTable(this.topics), this.util.makeHeading1('Abstract'), this.util.makeParagraph(this.abstract), this.util.makeHeading1('Company Detail'), - companySection.makeFirmographics() + companySection.makeFirmographicsDOCX() ]) // Construct the document diff --git a/src/report/themes.js b/src/report/themes.js deleted file mode 100644 index 9343338..0000000 --- a/src/report/themes.js +++ /dev/null @@ -1,127 +0,0 @@ -// Import modules -import docx from 'docx' -import Utilities from './common.js' - -class KeyThemes { - constructor (themeType, themes, quotes, interactions) { - this.type = themeType - this.themes = themes ? themes : false - this.quotes = quotes ? quotes : false - this.interactions = interactions ? interactions : false - this.font = 'Avenir Next' // We need to pass this in from the config file - this.fontSize = 10 // We need to pass this in from the config file - this.fontFactor = 1.5 - this.util = new Utilities() - this.introduction = 'The mediumroast.io system has automatically generated this section. ' + - 'If this section is for a summary theme, then two sections are included: ' + - '1. Information for the summary theme including key words, score and rank. ' + - '2. Excerpts from the interactions within the sub-study and links to each interaction reference.' - } - - basicThemeRow (theme, score, rank, bold) { - // return the row - return new docx.TableRow({ - children: [ - new docx.TableCell({ - width: { - size: 60, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.util.makeParagraph(theme, this.fontFactor * this.fontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.util.makeParagraph(score, this.fontFactor * this.fontSize, bold ? true : false)] - }), - new docx.TableCell({ - width: { - size: 20, - type: docx.WidthType.PERCENTAGE, - font: this.font, - }, - children: [this.util.makeParagraph(rank, this.fontFactor * this.fontSize, bold ? true : false)] - }), - ] - }) - } - - // Create the table for the doc - summaryThemeTable(themes) { - let myRows = [this.basicThemeRow('Topic Keywords', 'Score', 'Rank', true)] - for (const theme in themes) { - myRows.push(this.basicThemeRow(theme, themes[theme].score.toFixed(2), themes[theme].rank)) - } - // define the table with the summary theme information - const myTable = new docx.Table({ - columnWidths: [60, 20, 20], - rows: myRows, - width: { - size: 100, - type: docx.WidthType.PERCENTAGE - } - }) - - return myTable - } - - getInteractionName (guid, interactions) { - for (const interaction in interactions) { - if (String(guid) === String(interactions[interaction].GUID)) { - return interactions[interaction].interactionName - } else { - continue - } - } - return 'Unknown Interaction Name' - } - - // Create the theme quotes/excerpts for the doc - summaryQuotes(quotes, interactions) { - let myQuotes = [] - for (const quote in quotes) { - const myInteraction = this.getInteractionName(quote, interactions) - myQuotes.push( - this.util.makeParagraph('"' + quotes[quote].quotes[0] + '"', this.fontFactor * this.fontSize, false, 0), - new docx.Paragraph({ - children: [ - this.util.makeTextrun('Source: '), - // TODO open a GitHub issue for this behavior: - // A bookmark can only handle a substring from 0:40, and it will automatically - // truncate to that length. However when there is an internal hyperlink there is - // no similar truncation. This leads to the internal hyperlink having an incorrect - // reference. - // NOTE move the substring into the method to create an internal hyperlink - this.util.makeInternalHyperLink(myInteraction, String(quote).substring(0,40)), - this.util.makeTextrun('', 2), - ], - }) - ) - } - return myQuotes - } - - makeDocx () { - if (this.type === 'summary') { - const excerpts = this.summaryQuotes(this.quotes, this.interactions) - return [ - this.util.pageBreak(), - this.util.makeHeading1('Summary Theme: Table and Excerpts'), - this.util.makeParagraph(this.introduction, 0), - this.util.makeHeading2('Summary Theme Table'), - this.summaryThemeTable(this.themes), - this.util.makeBookmark2('Summary Theme Excerpts', 'summary_excerpts'), - ...excerpts - ] - } else { - return [] - } - - } -} - -export default KeyThemes \ No newline at end of file