-
Notifications
You must be signed in to change notification settings - Fork 92
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-123395/implement-create-command #691
Merged
hugomontero
merged 15 commits into
feature/logic-function
from
feature/sc-123395/implement-create-command
Dec 5, 2023
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
474e69d
add template processor for copy and replacement of logic-function tem…
hugomontero 0e904d0
add hasTemplateFiles validation function
hugomontero e35d548
implement logic-function create command
hugomontero 0ba07aa
fix tests for windows/linux
hugomontero f8e2bf8
add restore sinon after each test
hugomontero f6d8e2a
rename assets template files
hugomontero f1c546c
refactor template processor to handle file by file and allow file ren…
hugomontero f224b2f
ensure the dir in template creation
hugomontero 4655562
implement new template creation function with name replacement
hugomontero 7eda5a5
add trim to name
hugomontero afcc76e
add @types to creation template
hugomontero 6725d64
implement recursion to visit every file into every dir from templates
hugomontero c79c251
add fixme for hasTemplateFiles
hugomontero 786952a
rename test name with the right name
hugomontero 348a583
use slugify utility
hugomontero File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
12
assets/logicFunction/logic_function_name.logic.json.template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
@@ -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} | ||
|
@@ -35,6 +40,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; | ||
} catch (e) { | ||
throw createAPIErrorResult({ error: e, message: 'Error listing logic functions' }); | ||
} | ||
|
@@ -46,9 +52,129 @@ module.exports = class LogicFunctionsCommand extends CLICommandBase { | |
console.log(org, name, id); | ||
} | ||
|
||
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 }){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be refactored. I'll do that in further branches |
||
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 } }) { | ||
|
@@ -92,7 +218,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()` | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, This can directly use the
list
method from this class.Until we may have an API to get the name and id separately, I added this internal function to fetch the full list of the logic functions, and obtain the
name
ifid
is given and vice versa. @hugomontero raised a concern about downloading a potentially large list of logic functions and how that could impact the behavior or performance.I made the above changes in the future branch: https://github.com/particle-iot/particle-cli/tree/feature/logic-function-get