Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/sc-123396/implement-execute-command-with-eventdata #692

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/cli/logic-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ module.exports = ({ commandProcessor, root }) => {
},
'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');
Expand Down
8 changes: 8 additions & 0 deletions src/cmd/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,14 @@ module.exports = class ParticleApi {
}));
}

executeLogicFunction({ org, logic }) {
return this._wrap(this.api.executeLogic({
org: org,
logic: logic,
auth: this.accessToken,
}));
}

_wrap(promise){
return Promise.resolve(promise)
.then(result => result.body || result)
Expand Down
113 changes: 99 additions & 14 deletions src/cmd/logic-function.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const ParticleAPI = require('./api');
const CLICommandBase = require('./base');
const VError = require('verror');
const settings = require('../../settings');
const { normalizedApiError } = require('../lib/api-client');
const templateProcessor = require('../lib/template-processor');
const fs = require('fs-extra');
const { slugify } = require('../lib/utilities');

const logicFunctionTemplatePath = path.join(__dirname, '/../../assets/logicFunction');

/**
Expand Down Expand Up @@ -50,24 +51,20 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {

async get({ org, name, id }) {
// 1. Get the list of logic functions to download from
const list = await this.list()

//const list = await this.list();
console.log(org, name, id);
// 2. Select one using picker

// 3. Download it to files. Take care of formatting

// 4.
// 4.

}

async create({ org, name, params : { filepath } } = { params: { } }) {
const orgName = getOrgName(org);
const api = createAPI();
// get name from filepath
if (!filepath) {
// use default directory
filepath = '.';
}
const logicPath = getFilePath(filepath);
if (!name) {
const question = {
type: 'input',
Expand All @@ -88,7 +85,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {
const result = await this.ui.prompt([question]);
const description = result.description;
const slugName = slugify(name);
const destinationPath = path.join(filepath, slugName);
const destinationPath = path.join(logicPath, slugName);

this.ui.stdout.write(`Creating Logic Function ${this.ui.chalk.bold(name)} for ${orgName}...${os.EOL}`);
await this._validateExistingName({ api, org, name });
Expand All @@ -100,7 +97,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {
templatePath: logicFunctionTemplatePath,
destinationPath: path.join(filepath, slugName)
});
this.ui.stdout.write(`Successfully created ${this.ui.chalk.bold(name)} in ${this.ui.chalk.bold(filepath)}${os.EOL}`);
this.ui.stdout.write(`Successfully created ${this.ui.chalk.bold(name)} in ${this.ui.chalk.bold(logicPath)}${os.EOL}`);
this.ui.stdout.write(`Files created:${os.EOL}`);
createdFiles.forEach((file) => {
this.ui.stdout.write(`- ${file}${os.EOL}`);
Expand Down Expand Up @@ -185,9 +182,89 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {
return createdFiles;
}

async execute({ org, data, params: { filepath } }) {
// TODO
console.log(org, data, filepath);
async execute({ org, data, dataPath, params: { filepath } }) {
let logicData;
const orgName = getOrgName(org);
const logicPath = getFilePath(filepath);

if (!data && !dataPath) {
throw new Error('Error: Please provide either data or dataPath');
}
if (data && dataPath) {
throw new Error('Error: Please provide either data or dataPath');
}
if (dataPath) {
logicData = await fs.readFile(dataPath, 'utf8');
} else {
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 logic = {
event: {
event_data: logicData,
event_name: 'test_event',
device_id: '',
product_id: 0
},
Comment on lines +209 to +214
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too restrictive for CLI testing. We'll definitely want to allow passing device id, product id, event name and event data. We'll also want to be able to pass other types of triggers like scheduled and Ledger change. But for now that's ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I wanted to put two params: --deviceId and --productId in order to make the user can test different devices/products according their needs. I didn't that now given that I wasn't sure the best ui interaction for asking that info.
Thinking about a prompt or just adding that into a option param

source: {
type: configurationFile.logic_function.source.type,
code: logicCodeContent
}
};
const api = createAPI();
try {
this.ui.stdout.write(`Executing Logic Function ${this.ui.chalk.bold(logicCodeFileName)} for ${orgName}...${os.EOL}`);
const { result } = await api.executeLogicFunction({ org, logic, data });
const resultType = result.status === 'Success' ? this.ui.chalk.cyanBright(result.status) : this.ui.chalk.red(result.status);
this.ui.stdout.write(`Execution Status: ${resultType}${os.EOL}`);
this.ui.stdout.write(`Logs of the Execution:${os.EOL}`);
result.logs.forEach((log, index) => {
this.ui.stdout.write(` ${index + 1}.- ${JSON.stringify(log)}${os.EOL}`);
});
if (result.err) {
this.ui.stdout.write(this.ui.chalk.red(`Error during Execution:${os.EOL}`));
this.ui.stdout.write(`${result.err}${os.EOL}`);
} else {
this.ui.stdout.write(this.ui.chalk.cyanBright(`No errors during Execution.${os.EOL}`));
}
} catch (error) {
throw createAPIErrorResult({ error: error, message: `Error executing logic function for ${orgName}` });
}
}

async _pickLogicFunctionFileByExtension({ logicPath, files, extension = 'js' } ) {
let fileName;
const filteredFiles = findFilesByExtension(files, extension);
if (filteredFiles.length === 0) {
throw new Error(`Error: No ${extension} files found in ${logicPath}`);
}
if (filteredFiles.length === 1) {
fileName = filteredFiles[0];
} else {
const choices = filteredFiles.map((file) => {
return {
name: file,
value: file
};
});
const question = {
type: 'list',
name: 'file',
message: `Which ${extension} file would you like to use?`,
choices
};
const result = await this.ui.prompt([question]);
fileName = result.file;
}

const fileBuffer = await fs.readFile(path.join(logicPath, fileName));
return { fileName, content: fileBuffer.toString() };
}

async deploy({ org, params: { filepath } }) {
Expand Down Expand Up @@ -229,6 +306,14 @@ function getOrgName(org) {
return org || 'Sandbox';
}

function getFilePath(filepath) {
return filepath || '.';
}

function findFilesByExtension(files, extension) {
return files.filter((file) => file.endsWith(`.${extension}`));
}

// TODO (mirande): reconcile this w/ `normalizedApiError()` and `ensureError()`
// utilities and pull the result into cmd/api.js
function formatAPIErrorMessage(error){
Expand Down
64 changes: 62 additions & 2 deletions src/cmd/logic-function.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ describe('LogicFunctionCommands', () => {
stderr: {
write: sinon.stub()
},
prompt: sinon.stub(),
chalk: {
bold: sinon.stub(),
cyanBright: sinon.stub(),
yellow: sinon.stub(),
grey: sinon.stub(),
red: sinon.stub()
},
};
});
Expand Down Expand Up @@ -177,10 +179,9 @@ describe('LogicFunctionCommands', () => {
logicFunctionCommands.ui.prompt.onCall(1).resolves({ description: 'Logic Function 1' });
let error;
try {
const files = await logicFunctionCommands.create({
await logicFunctionCommands.create({
params: { filepath: PATH_TMP_DIR }
});
console.log(files);
} catch (e) {
error = e;
}
Expand All @@ -189,4 +190,63 @@ describe('LogicFunctionCommands', () => {
});

});

describe('execute', () => {
it('executes a logic function with user provided data', async () => {
nock('https://api.particle.io/v1', )
.intercept('/logic/execute', 'POST')
.reply(200, { result: { status: 'Success', logs: [] } });
await logicFunctionCommands.execute({
params: { filepath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf1_proj') },
data: { foo: 'bar' }
});
expect(logicFunctionCommands.ui.stdout.write.callCount).to.equal(4);
expect(logicFunctionCommands.ui.chalk.bold.callCount).to.equal(1);
expect(logicFunctionCommands.ui.chalk.bold.firstCall.args[0]).to.equal('code.js'); // file name
expect(logicFunctionCommands.ui.chalk.cyanBright.callCount).to.equal(2);
expect(logicFunctionCommands.ui.chalk.cyanBright.firstCall.args[0]).to.equal('Success');
expect(logicFunctionCommands.ui.chalk.cyanBright.secondCall.args[0]).to.equal(`No errors during Execution.${os.EOL}`);
});
it('executes a logic function with user provided data from file', async () => {
nock('https://api.particle.io/v1', )
.intercept('/logic/execute', 'POST')
.reply(200, { result: { status: 'Success', logs: [] } });
await logicFunctionCommands.execute({
params: { filepath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf1_proj') },
dataPath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf1_proj', 'sample', 'data.json')
});
expect(logicFunctionCommands.ui.stdout.write.callCount).to.equal(4);
expect(logicFunctionCommands.ui.chalk.bold.callCount).to.equal(1);
expect(logicFunctionCommands.ui.chalk.bold.firstCall.args[0]).to.equal('code.js'); // file name
expect(logicFunctionCommands.ui.chalk.cyanBright.callCount).to.equal(2);
expect(logicFunctionCommands.ui.chalk.cyanBright.firstCall.args[0]).to.equal('Success');
expect(logicFunctionCommands.ui.chalk.cyanBright.secondCall.args[0]).to.equal(`No errors during Execution.${os.EOL}`);
});
it('executes a logic function with user provided data from file and shows error', async () => {
nock('https://api.particle.io/v1', )
.intercept('/logic/execute', 'POST')
.reply(200, { result: { status: 'Exception', logs: [], err: 'Error message' } });
await logicFunctionCommands.execute({
params: { filepath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf1_proj') },
dataPath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf1_proj', 'sample', 'data.json')
});
expect(logicFunctionCommands.ui.stdout.write.callCount).to.equal(5);
expect(logicFunctionCommands.ui.chalk.bold.firstCall.args[0]).to.equal('code.js'); // file name
expect(logicFunctionCommands.ui.stdout.write.lastCall.args[0]).to.equal(`Error message${os.EOL}`);
});
it('prompts if found multiple files', async () => {
nock('https://api.particle.io/v1', )
.intercept('/logic/execute', 'POST')
.reply(200, { result: { status: 'Success', logs: [] } });
logicFunctionCommands.ui.prompt = sinon.stub();
logicFunctionCommands.ui.prompt.onCall(0).resolves({ file: 'code.js' });
await logicFunctionCommands.execute({
params: { filepath: path.join(PATH_FIXTURES_LOGIC_FUNCTIONS, 'lf2_proj') },
data: { foo: 'bar' }
});
expect(logicFunctionCommands.ui.prompt.callCount).to.equal(1);
expect(logicFunctionCommands.ui.prompt.firstCall.lastArg[0].choices[0].name).to.equal('code.js');
expect(logicFunctionCommands.ui.prompt.firstCall.lastArg[0].choices[1].name).to.equal('code2.js');
});
});
});
7 changes: 7 additions & 0 deletions test/__fixtures__/logic_functions/lf1_proj/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @param {import('particle:core').FunctionContext} context
*/
export default function main() {
// Add your code here
console.log('Hello from logic function!');
}
12 changes: 12 additions & 0 deletions test/__fixtures__/logic_functions/lf1_proj/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "schemas/logic_function.schema.json",
"logic_function": {
"name": "my logic function",
"description": "my description",
"source": {
"type": "JavaScript"
},
"enabled": true,
"logic_triggers": []
}
}
3 changes: 3 additions & 0 deletions test/__fixtures__/logic_functions/lf1_proj/sample/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}
7 changes: 7 additions & 0 deletions test/__fixtures__/logic_functions/lf2_proj/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @param {import('particle:core').FunctionContext} context
*/
export default function main() {
// Add your code here
console.log('Hello from logic function!');
}
7 changes: 7 additions & 0 deletions test/__fixtures__/logic_functions/lf2_proj/code2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @param {import('particle:core').FunctionContext} context
*/
export default function main() {
// Add your code here
console.log('Hello from logic function!');
}
12 changes: 12 additions & 0 deletions test/__fixtures__/logic_functions/lf2_proj/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "schemas/logic_function.schema.json",
"logic_function": {
"name": "my logic function",
"description": "my description",
"source": {
"type": "JavaScript"
},
"enabled": true,
"logic_triggers": []
}
}