diff --git a/src/cli/logic-function.js b/src/cli/logic-function.js index 01155e346..7304227d6 100644 --- a/src/cli/logic-function.js +++ b/src/cli/logic-function.js @@ -76,7 +76,13 @@ module.exports = ({ commandProcessor, root }) => { 'org': { description: 'Specify the organization', hidden: true - } + }, + 'data': { + description: 'Sample test data file to verify the logic function' + }, + 'dataPath': { + description: 'Sample test data file to verify the logic function' + }, }, handler: (args) => { const LogicFunctionsCmd = require('../cmd/logic-function'); diff --git a/src/cmd/api.js b/src/cmd/api.js index 10b5e8cf4..28c5cf8fc 100644 --- a/src/cmd/api.js +++ b/src/cmd/api.js @@ -274,6 +274,14 @@ module.exports = class ParticleApi { })); } + createLogicFunction({ org, logicFunction }) { + return this._wrap(this.api.createLogicFunction({ + auth: this.accessToken, + org, + logicFunction + })); + } + _wrap(promise){ return Promise.resolve(promise) .then(result => result.body || result) diff --git a/src/cmd/logic-function.js b/src/cmd/logic-function.js index 98d60090f..42398360b 100644 --- a/src/cmd/logic-function.js +++ b/src/cmd/logic-function.js @@ -64,17 +64,17 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { const { logicFunctionConfigData, logicFunctionCode } = this._serializeLogicFunction(logicFunctionData); - const { dirPath, jsonPath, jsPath } = await this._generateFiles({ logicFunctionConfigData, logicFunctionCode, name }); + const { jsonPath, jsPath } = await this._generateFiles({ logicFunctionConfigData, logicFunctionCode, name }); - this._printGetOutput({ dirPath, jsonPath, jsPath }); + this._printGetOutput({ jsonPath, jsPath }); this._printGetHelperOutput(); } - _printGetOutput({ dirPath, jsonPath, jsPath }) { + _printGetOutput({ jsonPath, jsPath }) { this.ui.stdout.write(`${os.EOL}`); this.ui.stdout.write(`Downloaded:${os.EOL}`); - this.ui.stdout.write(` - ${path.basename(dirPath) + '/' + path.basename(jsonPath)}${os.EOL}`); - this.ui.stdout.write(` - ${path.basename(dirPath) + '/' + path.basename(jsPath)}${os.EOL}`); + this.ui.stdout.write(` - ${path.basename(jsonPath)}${os.EOL}`); + this.ui.stdout.write(` - ${path.basename(jsPath)}${os.EOL}`); this.ui.stdout.write(`${os.EOL}`); } @@ -165,9 +165,9 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { // Prompts the user to overwrite if any files exist // If user says no, we exit the process - async _validatePaths({ dirPath, jsonPath, jsPath, _exit = () => process.exit(0) }) { + async _validatePaths({ jsonPath, jsPath, _exit = () => process.exit(0) }) { let exists = false; - const pathsToCheck = [dirPath, jsonPath, jsPath]; + const pathsToCheck = [jsonPath, jsPath]; for (const p of pathsToCheck) { if (await fs.pathExists(p)) { exists = true; @@ -261,11 +261,8 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { logicData = data; } - const files = await fs.readdir(logicPath); - const { content: configurationFileString } = await this._pickLogicFunctionFileByExtension({ files, extension: 'json', logicPath }); - const configurationFile = JSON.parse(configurationFileString); - // TODO (hmontero): here we can pick different files based on the source type - const { fileName: logicCodeFileName, content: logicCodeContent } = await this._pickLogicFunctionFileByExtension({ files, logicPath }); + const { logicConfigContent } = await this._getLogicFunctionConfig({ logicPath }); + const { logicCodeFileName, logicCodeContent } = await this._getLogicFunctionCode({ logicPath }); const logic = { event: { @@ -275,7 +272,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { product_id: 0 }, source: { - type: configurationFile.logic_function.source.type, + type: logicConfigContent.logic_function.source.type, code: logicCodeContent } }; @@ -295,11 +292,25 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { } else { this.ui.stdout.write(this.ui.chalk.cyanBright(`No errors during Execution.${os.EOL}`)); } + return { logicConfigContent, logicCodeContent }; } catch (error) { throw createAPIErrorResult({ error: error, message: `Error executing logic function for ${orgName}` }); } } + async _getLogicFunctionConfig({ logicPath }) { + const files = await fs.readdir(logicPath); + const { fileName, content: configurationFileString } = await this._pickLogicFunctionFileByExtension({ files, extension: 'json', logicPath }); + const configurationFileJson = JSON.parse(configurationFileString); + return { logicConfigFileName: fileName, logicConfigContent: configurationFileJson }; + } + async _getLogicFunctionCode({ logicPath }) { + const files = await fs.readdir(logicPath); + // TODO (hmontero): here we can pick different files based on the source type + const { fileName: logicCodeFileName, content: logicCodeContent } = await this._pickLogicFunctionFileByExtension({ files, logicPath }); + return { logicCodeFileName, logicCodeContent }; + } + async _pickLogicFunctionFileByExtension({ logicPath, files, extension = 'js' } ) { let fileName; const filteredFiles = findFilesByExtension(files, extension); @@ -318,6 +329,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { const result = await this._prompt({ type: 'list', + name: 'file', message: `Which ${extension} file would you like to use?`, choices @@ -329,9 +341,71 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { return { fileName, content: fileBuffer.toString() }; } - async deploy({ org, params: { filepath } }) { - // TODO - console.log(org, filepath); + async deploy({ org, data, dataPath, params: { filepath } }) { + this._setOrg(org); + + await this._getLogicFunctionList(); + + const confirm = await this._prompt({ + type: 'confirm', + name: 'proceed', + message: `Deploying to ${getOrgName(this.org)}. Proceed?`, + choices: Boolean + }); + + if (!confirm.proceed) { + this.ui.stdout.write(`Aborted.${os.EOL}`); + return; + } + + const { logicConfigContent, logicCodeContent } = await this.execute({ org, data, dataPath, params: { filepath } }); + const name = logicConfigContent.logic_function.name; + logicConfigContent.logic_function.enabled = true; + logicConfigContent.logic_function.source.code = logicCodeContent; + + const logicFuncNameDeployed = await this._validateLFName({ name }); + if (logicFuncNameDeployed) { + try { + const confirm = await this._prompt({ + type: 'confirm', + name: 'proceed', + message: `A Logic Function with name ${name} is already available in the cloud ${getOrgName(this.org)}. Proceed and overwrite with the new content?`, + choices: Boolean + }); + + if (!confirm.proceed) { + this.ui.stdout.write(`Aborted.${os.EOL}`); + return; + } + + const { id } = await this._getLogicFunctionIdAndName(name); + await this.api.updateLogicFunction({ org, id, logicFunctionData: logicConfigContent.logic_function }); + this._printDeployOutput(name, id); + } catch (err) { + throw new Error(`Error deploying Logic Function ${name}: ${err.message}`); + } + } else { + try { + const deployedLogicFunc = await this.api.createLogicFunction({ org, logicFunction: logicConfigContent.logic_function }); + this._printDeployNewLFOutput(deployedLogicFunc.logic_function.name, deployedLogicFunc.logic_function.id); + } catch (err) { + throw new Error(`Error deploying Logic Function ${name}: ${err.message}`); + } + } + } + + async _printDeployOutput(name, id) { + this.ui.stdout.write(`${os.EOL}`); + this.ui.stdout.write(`Deploying Logic Function ${this.ui.chalk.bold(`${name} (${id})`)} to ${getOrgName(this.org)}...${os.EOL}`); + this.ui.stdout.write(`${this.ui.chalk.cyanBright('Success!')}${os.EOL}`); + this.ui.stdout.write(`${this.ui.chalk.yellow('Visit \'console.particle.io\' to view results from your device(s)!')}${os.EOL}`); + } + + async _printDeployNewLFOutput(name, id) { + this.ui.stdout.write(`${os.EOL}`); + this.ui.stdout.write(`Deploying Logic Function ${this.ui.chalk.bold(`${name}`)} to ${getOrgName(this.org)}...${os.EOL}`); + this.ui.stdout.write(`${this.ui.chalk.cyanBright(`Success! Logic Function ${name} deployed with ${id}`)}${os.EOL}`); + this.ui.stdout.write(`${this.ui.chalk.yellow('Visit \'console.particle.io\' to view results from your device(s)!')}${os.EOL}`); } async disable({ org, name, id }) { @@ -356,9 +430,9 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { } async _overwriteIfLFExistsLocally(name, id) { - const { dirPath, jsonPath, jsPath } = this._getLocalLFPathNames(name); + const { jsonPath, jsPath } = this._getLocalLFPathNames(name); - const exist = await this._validatePaths({ dirPath, jsonPath, jsPath }); + const exist = await this._validatePaths({ jsonPath, jsPath }); if (!exist) { return; @@ -368,20 +442,20 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { const { logicFunctionConfigData, logicFunctionCode } = this._serializeLogicFunction(logicFunctionData); - const { genDirPath, genJsonPath, genJsPath } = await this._generateFiles({ logicFunctionConfigData, logicFunctionCode, name }); + const { genJsonPath, genJsPath } = await this._generateFiles({ logicFunctionConfigData, logicFunctionCode, name }); - this._printDisableNewFilesOutput({ dirPath: genDirPath, jsonPath: genJsonPath, jsPath: genJsPath }); + this._printDisableNewFilesOutput({ jsonPath: genJsonPath, jsPath: genJsPath }); } _printDisableOutput(name, id) { this.ui.stdout.write(`Logic Function ${name}(${id}) is now disabled.${os.EOL}`); } - _printDisableNewFilesOutput({ dirPath, jsonPath, jsPath }) { + _printDisableNewFilesOutput({ jsonPath, jsPath }) { this.ui.stdout.write(`${os.EOL}`); this.ui.stdout.write(`The following files were overwritten after disabling the Logic Function:${os.EOL}`); - this.ui.stdout.write(` - ${path.basename(dirPath) + '/' + path.basename(jsonPath)}${os.EOL}`); - this.ui.stdout.write(` - ${path.basename(dirPath) + '/' + path.basename(jsPath)}${os.EOL}`); + this.ui.stdout.write(` - ${path.basename(jsonPath)}${os.EOL}`); + this.ui.stdout.write(` - ${path.basename(jsPath)}${os.EOL}`); this.ui.stdout.write(`${os.EOL}`); } @@ -445,30 +519,28 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { _serializeLogicFunction(data) { const logicFunctionCode = data.logic_function.source.code; const logicFunctionConfigData = data.logic_function; - delete logicFunctionConfigData.source; + delete logicFunctionConfigData.source.code; - return { logicFunctionConfigData, logicFunctionCode }; + return { logicFunctionConfigData: { 'logic_function': logicFunctionConfigData }, logicFunctionCode }; } async _generateFiles({ logicFunctionConfigData, logicFunctionCode, name }) { - const { dirPath, jsonPath, jsPath } = this._getLocalLFPathNames(name); + const { jsonPath, jsPath } = this._getLocalLFPathNames(name); - await this._validatePaths({ dirPath, jsonPath, jsPath }); + await this._validatePaths({ jsonPath, jsPath }); - await fs.ensureDir(dirPath); await fs.writeFile(jsonPath, JSON.stringify(logicFunctionConfigData, null, 2)); await fs.writeFile(jsPath, logicFunctionCode); - return { dirPath, jsonPath, jsPath }; + return { jsonPath, jsPath }; } _getLocalLFPathNames(name) { const slugName = slugify(name); - const dirPath = path.join(process.cwd(), `${slugName}`); - const jsonPath = path.join(dirPath, `${slugName}.logic.json`); - const jsPath = path.join(dirPath, `${slugName}.js`); + const jsonPath = path.join(process.cwd(), `${slugName}.logic.json`); + const jsPath = path.join(process.cwd(), `${slugName}.js`); - return { dirPath, jsonPath, jsPath }; + return { jsonPath, jsPath }; } async _getLogicFunctionIdAndName(name, id) { @@ -505,7 +577,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { throw new Error('Unable to get logic function id from name'); } - return found.id; // assume that found has a key called id + return found.id; } _getNameFromId(id, list) { @@ -515,7 +587,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { throw new Error('Unable to get logic function name from id'); } - return found.name; // assume that found has a key called name + return found.name; } }; diff --git a/src/cmd/logic-function.test.js b/src/cmd/logic-function.test.js index 3db91ceab..ab2e164ec 100644 --- a/src/cmd/logic-function.test.js +++ b/src/cmd/logic-function.test.js @@ -168,29 +168,27 @@ describe('LogicFunctionCommands', () => { const data = { logic_function : logicFunc1.logic_functions[0] }; const logicFunctionCode = data.logic_function.source.code; const logicFunctionConfigData = data.logic_function; - delete logicFunctionConfigData.source; sinon.stub(logicFunctionCommands, '_validatePaths').resolves(true); const name = 'LF1'; const slugName = slugify(name); - const { dirPath, jsonPath, jsPath } = await logicFunctionCommands._generateFiles({ logicFunctionConfigData, logicFunctionCode, name: 'LF1' }); + const { jsonPath, jsPath } = await logicFunctionCommands._generateFiles({ logicFunctionConfigData, logicFunctionCode, name: 'LF1' }); - expect(dirPath).to.eql(path.join(process.cwd(), slugName)); - expect(jsonPath).to.eql(path.join(process.cwd(), slugName, `${slugName}.logic.json`)); - expect(jsPath).to.eql(path.join(process.cwd(), slugName, `${slugName}.js`)); + expect(jsonPath).to.eql(path.join(process.cwd(), `${slugName}.logic.json`)); + expect(jsPath).to.eql(path.join(process.cwd(), `${slugName}.js`)); - await fs.remove(dirPath); + await fs.remove(jsonPath); + await fs.remove(jsPath); }); }); describe('_getLocalLFPathNames', async() => { it('returns local file paths where the LF should be saved', () => { const slugName = slugify('LF1'); - const { dirPath, jsonPath, jsPath } = logicFunctionCommands._getLocalLFPathNames('LF1'); + const { jsonPath, jsPath } = logicFunctionCommands._getLocalLFPathNames('LF1'); - expect(dirPath).to.eql(path.join(process.cwd(), slugName)); - expect(jsonPath).to.eql(path.join(process.cwd(), slugName, `${slugName}.logic.json`)); - expect(jsPath).to.eql(path.join(process.cwd(), slugName, `${slugName}.js`)); + expect(jsonPath).to.eql(path.join(process.cwd(), `${slugName}.logic.json`)); + expect(jsPath).to.eql(path.join(process.cwd(), `${slugName}.js`)); }); }); @@ -338,9 +336,7 @@ describe('LogicFunctionCommands', () => { it('returns if paths do not exist', async () => { sinon.stub(fs, 'pathExists').resolves(false); - const paths = ['dir/', 'dir/path/to/file']; - - const res = await logicFunctionCommands._validatePaths({ dirPath: paths[0], jsonPath: paths[1], _exit: sinon.stub() }); + const res = await logicFunctionCommands._validatePaths({ jsonPath: 'dir/path/to/file', _exit: sinon.stub() }); expect(res).to.eql(false); @@ -350,9 +346,8 @@ describe('LogicFunctionCommands', () => { const exitStub = sinon.stub(); sinon.stub(fs, 'pathExists').resolves(true); sinon.stub(logicFunctionCommands, '_promptOverwrite').resolves(false); - const paths = ['dir/', 'dir/path/to/file']; - await logicFunctionCommands._validatePaths({ dirPath: paths[0], jsonPath: paths[1], _exit: exitStub }); + await logicFunctionCommands._validatePaths({ jsonPath: 'dir/path/to/file', _exit: exitStub }); expect(logicFunctionCommands._promptOverwrite.callCount).to.eql(1); expect(exitStub.callCount).to.eql(1); @@ -731,4 +726,69 @@ describe('LogicFunctionCommands', () => { expect(logicFunctionCommands._printDisableOutput).to.not.have.been.called; }); }); + + describe('deploy', () => { + let logicFunctions = []; + logicFunctions.push(logicFunc1.logic_functions[0]); + + beforeEach(() => { + logicFunctionCommands.logicFuncList = logicFunctions; + }); + + afterEach(() => { + fs.rmSync('lf1', { recursive: true, force: true }); + nock.cleanAll(); + }); + + it('deploys a new logic function', async() => { + nock('https://api.particle.io/v1',) + .intercept('/logic/functions', 'POST') + .reply(200, { logic_function: logicFunc2.logic_functions[0] }); + sinon.stub(logicFunctionCommands, '_prompt').resolves({ proceed: true }); + sinon.stub(logicFunctionCommands, 'execute').resolves({ logicConfigContent: { logic_function: logicFunc2.logic_functions[0] }, logicCodeContent: logicFunc2.logic_functions[0].source.code }); + sinon.stub(logicFunctionCommands, '_printDeployNewLFOutput').resolves({ }); + + await logicFunctionCommands.deploy({ params: { filepath: 'test/lf1' } }); + + expect(logicFunctionCommands._prompt).to.have.property('callCount', 1); + expect(logicFunctionCommands.execute).to.have.been.calledOnce; + expect(logicFunctionCommands._printDeployNewLFOutput).to.have.been.calledOnce; + + }); + + it('re-deploys an old logic function', async() => { + nock('https://api.particle.io/v1',) + .intercept('/logic/functions/0021e8f4-64ee-416d-83f3-898aa909fb1b', 'PUT') + .reply(200, { logic_function: logicFunc1.logic_functions[0] }); + sinon.stub(logicFunctionCommands, '_prompt').resolves({ proceed: true }); + sinon.stub(logicFunctionCommands, 'execute').resolves({ logicConfigContent: { logic_function: logicFunc1.logic_functions[0] }, logicCodeContent: logicFunc1.logic_functions[0].source.code }); + sinon.stub(logicFunctionCommands, '_printDeployOutput').resolves({ }); + + await logicFunctionCommands.deploy({ params: { filepath: 'test/lf1' } }); + + expect(logicFunctionCommands._prompt).to.have.property('callCount', 2); + expect(logicFunctionCommands.execute).to.have.been.calledOnce; + expect(logicFunctionCommands._printDeployOutput).to.have.been.calledOnce; + }); + + it('throws an error if deployement fails', async() => { + nock('https://api.particle.io/v1',) + .intercept('/logic/functions/0021e8f4-64ee-416d-83f3-898aa909fb1b', 'PUT') + .reply(500, { error: 'Error' }); + sinon.stub(logicFunctionCommands, '_prompt').resolves({ proceed: true }); + sinon.stub(logicFunctionCommands, 'execute').resolves({ logicConfigContent: { logic_function: logicFunc1.logic_functions[0] }, logicCodeContent: logicFunc1.logic_functions[0].source.code }); + sinon.stub(logicFunctionCommands, '_printDeployOutput').resolves({ }); + + let error; + try { + await logicFunctionCommands.deploy({ params: { filepath: 'test/lf1' } }); + } catch (e) { + error = e; + } + + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.contain('Error deploying Logic Function LF1'); + + }); + }); });