Skip to content

Commit

Permalink
Merge pull request #691 from particle-iot/feature/sc-123395/implement…
Browse files Browse the repository at this point in the history
…-create-command

feature/sc-123395/implement-create-command
  • Loading branch information
hugomontero authored Dec 5, 2023
2 parents 1537773 + 348a583 commit f02a17e
Show file tree
Hide file tree
Showing 10 changed files with 561 additions and 26 deletions.
109 changes: 109 additions & 0 deletions assets/logicFunction/@types/particle_core.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
declare module 'particle:core' {
namespace Particle {
/***********************************************LEDGER**************************************************************/
export const MERGE: "Merge";
export const REPLACE: "Replace";
export interface Ledger {
/**
* Gets the current value of the ledger.
* Empty ledger returns {}, error returns null
* @returns {Object} the current value of the ledger
* @throws {Error} if the ledger doesn't exist or there was an error getting the ledger instance
*/
get(): { updatedAt: string, data: Record<string, any> },

/**
* Sets the value of the ledger. If the ledger does not exist, it will be created.
*
* @param data The data you would like to put into the ledger
* @param setMode Whether to replace or merge the new data into the ledger. Defaults to Replace. You can use the constants MERGE and REPLACE here
* @throws {Error} if the ledger doesn't exist or there was an error setting the ledger instance
*/
set(data: Record<string, any>, setMode: "Replace" | "Merge"): null,

/**
* Deletes the ledger data
* @throws {Error} if there was an error deleting the ledger instance
*/
delete(): null
}
/**
* Gets a ledger object which allows you to store data in the cloud. The scope value will automatically be picked
* based on the trigger's context. If you want to use a different scope value, you can provide a deviceId or productId
* depending on your needs in the second param.
*
* If the trigger contains a deviceId and productId, we will pass those to ledger and use what the ledger definition requires
*
* @param name a unique name regardless of the definition's scope
* @param scopeValues the override scope values if you want to ignore the logic trigger or are using a scheduled logic trigger
*/
export function ledger(name: string, scopeValues?: { deviceId?: string, productId?: number }): Ledger;

/***********************************************CONTEXT*************************************************************/
export interface FunctionInfo {
ownerId: string;
logicFunctionId: string;
}

export interface TriggerInfo {
triggerEventId?: string;
triggeredAt: string;
}

export interface EventInfo {
publishedAt: string;
eventName: string;
eventData?: string;
deviceId: string;
productId?: number;
userId?: string;
}

export interface ScheduledInfo {
scheduledAt: string;
startAt: string;
endAt?: string;
}

export interface LedgerChangeInfo {
changedAt: string;
changeType: 'set' | 'delete';
name: string;
scope: string;
data: Record<string, any>
}

/**
* The context object that is passed to the function when it is invoked.
* The event, scheduled and ledgerChange properties will only be present if the function was invoked by that type of trigger
* and only one of them will be present at a time (logic functions can only have one type of trigger).
*/
export interface FunctionContext {
functionInfo: FunctionInfo;
trigger: TriggerInfo;
event?: EventInfo;
scheduled?: ScheduledInfo;
ledgerChange?: LedgerChangeInfo;
}

/**
* Returns an object containing the raw context, trigger and its details.
*/
export function getContext(): FunctionContext;

/***********************************************PUBLISH*************************************************************/
/**
* Synchronously emits a Particle event message. Note that non-string values for `data` will be stringified
*
* @param name The name of the event
* @param data The particle publish data
* @param options Allows specifying how the data is published
* @param options.productId Publish to the event stream of this product
* @param options.asDeviceId Publish the event as if it came from this device.
* @throws {Error} if there was an error publishing the event
*/
export function publish(name: string, data: any | undefined, options: { productId: number, asDeviceId: string }): null;
}

export = Particle;
}
42 changes: 42 additions & 0 deletions assets/logicFunction/@types/particle_encoding.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
declare module 'particle:encoding' {
namespace Encoding {
/**
* Converts a byte array to a UTF-8 string
* @param input The byte array to convert
* @returns {string} The converted string
*/
export function bytesToString(input: number[]): string;
/**
* Converts a UTF-8 string to a byte array
* @param input The string to convert
* @returns {number[]} The byte array representing this string
*/
export function stringToBytes(input: string): number[];
/**
* Encodes a string or byte array to base64 (RFC 3548)
* @param input The string or byte array to encode
* @returns {string} The base64 encoded string
*/
export function base64Encode(input: string | number[]): string;
/**
* Decodes a base64 (RFC 3548) string to a byte array
* @param input The base64 string to decode
* @returns {number[]} The decoded byte array
*/
export function base64Decode(input: string): number[];
/**
* Encodes a string or byte array to base85 (RFC1924)
* @param input The string or byte array to encode
* @returns {string} The base85 encoded string
*/
export function base85Encode(input: string | number[]): string;
/**
* Decodes a base85 (RFC1924) string to a byte array
* @param input The base85 string to decode
* @returns {number[]} The decoded byte array
*/
export function base85Decode(input: string): number[];
}

export = Encoding;
}
5 changes: 5 additions & 0 deletions assets/logicFunction/logic_function_name.js.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Particle from 'particle:core';

export default function process({ functionInfo, trigger, event }) {
// Add your code here
}
12 changes: 12 additions & 0 deletions assets/logicFunction/logic_function_name.logic.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "schemas/logic_function.schema.json",
"logic_function": {
"name": "${name}",
"description": "${description}",
"source": {
"type": "JavaScript"
},
"enabled": true,
"logic_triggers": []
}
}
5 changes: 4 additions & 1 deletion src/cli/logic-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ module.exports = ({ commandProcessor, root }) => {
options: {
'org': {
description: 'Specify the organization'
}
},
'name': {
description: 'Name of the logic function'
},
},
handler: (args) => {
const LogicFunctionsCmd = require('../cmd/logic-function');
Expand Down
140 changes: 133 additions & 7 deletions src/cmd/logic-function.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
const os = require('os');
const path = require('path');
const ParticleAPI = require('./api');
const CLICommandBase = require('./base');
const VError = require('verror');
const settings = require('../../settings');
const { normalizedApiError } = require('../lib/api-client');
const CLICommandBase = require('./base');
const os = require('os');
const templateProcessor = require('../lib/template-processor');
const fs = require('fs-extra');
const { slugify } = require('../lib/utilities');
const logicFunctionTemplatePath = path.join(__dirname, '/../../assets/logicFunction');

/**
* Commands for managing encryption keys.
Expand All @@ -19,7 +24,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {
try {
const logicFunctions = await api.getLogicFunctionList({ org });
const orgName = getOrgName(org);
const list = logicFunctions['logic_functions'];
const list = logicFunctions.logic_functions;
if (list.length === 0) {
this.ui.stdout.write(`No Logic Functions currently deployed in your ${orgName}.`);
this.ui.stdout.write(`To create a Logic Function, see \`particle logic-function create\`.${os.EOL}
Expand All @@ -36,6 +41,7 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {
this.ui.stdout.write(`${os.EOL}To view a Logic Function's code, see \`particle lf get.\`${os.EOL}`);
return list;
}
return list;
} catch (e) {
throw createAPIErrorResult({ error: e, message: 'Error listing logic functions' });
}
Expand All @@ -54,9 +60,129 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase {

}

async create({ org, params: { filepath } }) {
// TODO
console.log(org, filepath);
async create({ org, name, params : { filepath } } = { params: { } }) {
const orgName = getOrgName(org);
const api = createAPI();
// get name from filepath
if (!filepath) {
// use default directory
filepath = '.';
}
if (!name) {
const question = {
type: 'input',
name: 'name',
message: 'What would you like to call your Function?'
};
const result = await this.ui.prompt([question]);
name = result.name;
}
// trim name
name = name.trim();
// ask for description
const question = {
type: 'input',
name: 'description',
message: 'Add a description for your Function (optional)'
};
const result = await this.ui.prompt([question]);
const description = result.description;
const slugName = slugify(name);
const destinationPath = path.join(filepath, slugName);

this.ui.stdout.write(`Creating Logic Function ${this.ui.chalk.bold(name)} for ${orgName}...${os.EOL}`);
await this._validateExistingName({ api, org, name });
await this._validateExistingFiles({ templatePath: logicFunctionTemplatePath, destinationPath });
const createdFiles = await this._copyAndReplaceLogicFunction({
logicFunctionName: name,
logicFunctionSlugName: slugName,
description,
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(`Files created:${os.EOL}`);
createdFiles.forEach((file) => {
this.ui.stdout.write(`- ${file}${os.EOL}`);
});
this.ui.stdout.write(`${os.EOL}Guidelines for creating your Logic Function can be found <TBD>.${os.EOL}`);
this.ui.stdout.write(`Once you have written your Logic Function, run${os.EOL}`);
this.ui.stdout.write(`- \`particle logic execute\` to run your Function${os.EOL}`);
this.ui.stdout.write(`- \`particle logic deploy\` to deploy your new changes${os.EOL}`);
return createdFiles;
}

async _validateExistingName({ api, org, name }) {
// TODO (hmontero): request for a getLogicFunctionByName() method in the API
let existingLogicFunction;
try {
const logicFunctionsResponse = await api.getLogicFunctionList({ org });
const existingLogicFunctions = logicFunctionsResponse.logic_functions;
existingLogicFunction = existingLogicFunctions.find((item) => item.name === name);
} catch (error) {
this.ui.stdout.write(this.ui.chalk.yellow(`Warn: We were unable to check if a Logic Function with name ${name} already exists.${os.EOL}`));
}
if (existingLogicFunction) {
throw new Error(`Error: Logic Function with name ${name} already exists.`);
}
}

async _validateExistingFiles({ templatePath, destinationPath }){
const filesAlreadyExist = await templateProcessor.hasTemplateFiles({
templatePath,
destinationPath
});
if (filesAlreadyExist) {
const question = {
type: 'confirm',
name: 'overwrite',
message: `We found existing files in ${this.ui.chalk.bold(destinationPath)}. Would you like to overwrite them?`
};
const { overwrite } = await this.ui.prompt([question]);
if (!overwrite) {
this.ui.stdout.write(`Aborted.${os.EOL}`);
process.exit(0);
}
}
}

/** Recursively copy and replace template files */
async _copyAndReplaceLogicFunction({ logicFunctionName, logicFunctionSlugName, description, templatePath, destinationPath }){
const files = await fs.readdir(templatePath);
const createdFiles = [];

for (const file of files){
//createdFiles.push(destinationFile);
// check if file is a dir
const stat = await fs.stat(path.join(templatePath, file));
if (stat.isDirectory()) {
const subFiles = await this._copyAndReplaceLogicFunction({
logicFunctionName,
logicFunctionSlugName,
description,
templatePath: path.join(templatePath, file),
destinationPath: path.join(destinationPath, file)
});
createdFiles.push(...subFiles);
} else {
const fileReplacements = {
'logic_function_name': logicFunctionSlugName,
};
const destinationFile = await templateProcessor.copyAndReplaceTemplate({
fileNameReplacements: fileReplacements,
file,
templatePath,
destinationPath,
replacements: {
name: logicFunctionName,
description: description || ''
}
});
createdFiles.push(destinationFile);
}
}
// return file name created
return createdFiles;
}

async execute({ org, data, params: { filepath } }) {
Expand Down Expand Up @@ -100,7 +226,7 @@ function createAPIErrorResult({ error: e, message, json }){

// get org name from org slug
function getOrgName(org) {
return org || 'Staging';
return org || 'Sandbox';
}

// TODO (mirande): reconcile this w/ `normalizedApiError()` and `ensureError()`
Expand Down
Loading

0 comments on commit f02a17e

Please sign in to comment.