particle-cli
Version:
Simple Node commandline application for working with your Particle devices and using the Particle Cloud
370 lines (334 loc) • 10.5 kB
JavaScript
const path = require('path');
const fs = require('fs-extra');
const ParticleAPI = require('../cmd/api');
const settings = require('../../settings');
const VError = require('verror');
const { normalizedApiError } = require('./api-client');
const { slugify, globList } = require('./utilities');
const templateProcessor = require('./template-processor');
class LogicFunction {
constructor({ org, name, id, description, type, enabled, _path, version, triggers, api = createAPI() }) {
// throw if api is not provided
this.org = org;
this.name = name;
this.description = description || '';
this.id = id;
this.api = api;
this._path = _path || process.cwd();
this.enabled = !!enabled;
this.version = version || 0;
this.triggers = triggers || [];
this.type = type || 'JavaScript',
this.files = {
sourceCode: {
name: name ? slugify(name) + '.js' : '',
content: ''
}, // might change once we implement execute method
configuration: {
name: name ? slugify(name) + '.logic.json' : '',
content: ''
},
types:[] // should be an array of { name: 'type name', content: 'type content' }
};
}
static async listFromCloud({ org, api = createAPI() } = {}) {
try {
const response = await api.getLogicFunctionList({ org: org });
const logicFunctions = response.logic_functions.map(logicFunctionData => {
const lf = new LogicFunction({
org,
...logicFunctionData,
triggers: logicFunctionData.logic_triggers,
api
});
const source = logicFunctionData.source;
lf.files.sourceCode.content = source ? source.code : '';
return lf;
});
return logicFunctions;
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error listing Logic Functions' });
}
}
static async listFromDisk({ filepath, org, api = createAPI() } = {}) {
let logicFunctions = [];
let malformedLogicFunctions = [];
if (!filepath) {
filepath = process.cwd();
}
const pathExists = await fs.exists(filepath);
const logicFunctionExtensionPattern = '*.logic.json';
if (!pathExists) {
throw new Error('Path does not exist');
}
// check if the path is a directory or a file
const stats = await fs.stat(filepath);
if (stats.isFile()) {
try {
// if it is a file, then load it as a Logic Function
const lf = await LogicFunction.loadFromDisk({
basePath: path.dirname(filepath),
fileName: path.basename(filepath),
org,
api
});
logicFunctions.push(lf);
} catch (error) {
malformedLogicFunctions.push({
name: path.basename(filepath.substring(0, filepath.indexOf('.'))),
error: error.message
});
}
} else {
const files = globList(filepath, [logicFunctionExtensionPattern]);
for (const file of files) {
// if the file is a directory, then load the Logic Functions from the directory
try {
const lf = await LogicFunction.loadFromDisk({
basePath: filepath,
fileName: path.basename(file),
org,
api
});
logicFunctions.push(lf);
} catch (error) {
malformedLogicFunctions.push({
name: path.basename(file.substring(0, file.indexOf('.'))),
error: error.message
});
}
}
}
return {
logicFunctions,
malformedLogicFunctions
};
}
static async loadFromDisk({ basePath, fileName, org, api = createAPI() } = {}) {
// if receive a .js then look for a .logic.json
// we need both files to be present
const baseFileName = fileName.substring(0, fileName.indexOf('.'));
const codeFile = `${baseFileName}.js`;
const configFile = `${baseFileName}.logic.json`;
const files = [codeFile, configFile];
for (const file of files) {
const filePath = path.join(basePath, file);
const pathExists = await fs.exists(filePath);
if (!pathExists) {
throw new Error(`File ${file} does not exist`);
}
}
const configuration = await fs.readFile(path.join(basePath, configFile), 'utf8');
const code = await fs.readFile(path.join(basePath, codeFile), 'utf8');
const logicFunction = new LogicFunction({ org, api });
logicFunction.files.configuration.name = configFile;
logicFunction.files.sourceCode.name = codeFile;
logicFunction.files.configuration.content = configuration;
logicFunction.files.sourceCode.content = code;
logicFunction.path = basePath;
logicFunction._deserializeConfiguration();
return logicFunction;
}
// should return an instance of LogicFunction
static async getByIdOrName({ id, name, list }) {
const logicFunctionData = list.find(lf => lf.id === id || lf.name === name);
if (!logicFunctionData) {
throw new Error('Logic Function not found');
}
return logicFunctionData;
}
async execute(trigger) {
const logicEvent = {
event: trigger.event,
source: {
type: this.type,
code: this.files.sourceCode.content
}
};
try {
const { result } =
await this.api.executeLogicFunction({ org: this.org, logic: logicEvent });
return {
logs: result.logs,
error: result.err,
status: result.status,
};
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error executing Logic Function' });
}
}
async deploy() {
const logicFunctionRequestData = {
name: this.name,
description: this.description,
enabled: this.enabled,
source: {
type: this.type,
code: this.files.sourceCode.content
},
logic_triggers: this.triggers
};
if (this.id) {
return this.updateToCloud(logicFunctionRequestData);
} else {
return this.createToCloud(logicFunctionRequestData);
}
}
async createToCloud(logicFunctionData) {
try {
const result = await this.api.createLogicFunction({
org: this.org,
logicFunction: logicFunctionData,
});
this.id = result.logic_function.id;
this.version = result.logic_function.version;
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error deploying Logic Function' });
}
}
async updateToCloud(logicFunctionData) {
try {
const result = await this.api.updateLogicFunction({
org: this.org,
id: this.id,
logicFunctionData: logicFunctionData,
});
this.version = result.logic_function.version;
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error deploying Logic Function' });
}
}
async deleteFromCloud() {
try {
await this.api.deleteLogicFunction({
org: this.org,
id: this.id
});
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error deleting Logic Function' });
}
}
copyFromOtherLogicFunction(logicFunction) {
this.name = logicFunction.name;
this.org = logicFunction.org;
this.description = logicFunction.description;
this.enabled = logicFunction.enabled;
this.type = logicFunction.type;
this.triggers = logicFunction.triggers;
this.files.sourceCode.content = logicFunction.files.sourceCode.content;
this.files.configuration.content = logicFunction.files.configuration.content;
}
async initFromTemplate({ templatePath }) {
const contentReplacements = {
name: this.name,
description: this.description
};
const fileNameReplacements = [
{ template: 'logic_function_name', fileName: slugify(this.name) },
];
const files = await templateProcessor.loadTemplateFiles({
templatePath,
contentReplacements,
fileNameReplacements
});
// remove the template path from the file names so that they are relative to the Logic Function path
files.forEach(file => {
file.fileName = file.fileName.replace(templatePath, '');
});
// put the data into the Logic Function
const sourceCode = files.find(file => file.fileName.includes(this.files.sourceCode.name));
this.files.sourceCode.name = sourceCode.fileName;
this.files.sourceCode.content = sourceCode.content;
const configuration = files.find(file => file.fileName.includes(this.files.configuration.name));
this.files.configuration.name = configuration.fileName;
this.files.configuration.content = configuration.content;
// add types
this.files.types = files.filter(file => file.fileName.includes('@types')).map(file => {
return {
name: file.fileName,
content: file.content
};
});
this._deserializeConfiguration();
}
get configurationPath () {
return path.join(this._path, this.files.configuration.name);
}
get sourcePath () {
return path.join(this._path, this.files.sourceCode.name);
}
set path (value) {
this._path = value || process.cwd();
}
get path () {
return this._path;
}
async saveToDisk(){
// ensure that the directory exists
fs.ensureDirSync(this.path);
// deserialize the Logic Function to config json
const configuration = this._toJSONString();
// save the config json to disk
await fs.writeFile(this.configurationPath, configuration);
// save the source code to disk
await fs.writeFile(this.sourcePath, this.files.sourceCode.content);
// save the types
for (const type of this.files.types) {
const dirPath = path.join(this.path, path.dirname(type.name));
fs.ensureDirSync(dirPath);
await fs.writeFile(path.join(dirPath, path.basename(type.name)), type.content);
}
}
_toJSONString(){
return JSON.stringify({
logic_function: {
id: this.id,
name: this.name,
description: this.description,
version: this.version,
enabled: this.enabled,
source: {
type: this.type,
},
logic_triggers: this.triggers
}
}, null, 2);
}
_deserializeConfiguration(){
const { logic_function: data } = JSON.parse(this.files.configuration.content);
this.id = data.id;
this.name = data.name;
this.description = data.description;
this.version = data.version;
this.enabled = data.enabled;
this.type = data.source.type;
this.triggers = data.logic_triggers;
}
}
function createAPI() {
return new ParticleAPI(settings.apiUrl, {
accessToken: settings.access_token
});
}
function createAPIErrorResult({ error: e, message, json }){
const error = new VError(formatAPIErrorMessage(e), message);
error.asJSON = json;
return error;
}
function formatAPIErrorMessage(error){
error = normalizedApiError(error);
if (error.body){
if (typeof error.body.error === 'string'){
error.message = error.body.error;
} else if (Array.isArray(error.body.errors)){
if (error.body.errors.length === 1){
error.message = error.body.errors[0];
}
}
}
if (error.message.includes('That belongs to someone else.')){
error.canRequestTransfer = true;
}
return error;
}
module.exports = LogicFunction;