From 4e8f3ed0266f729fecfb2287dbe14abe15eeb410 Mon Sep 17 00:00:00 2001 From: sw-yx Date: Mon, 8 Jul 2019 01:07:22 -0400 Subject: [PATCH 1/2] add functions:trigger command --- src/commands/functions/trigger.js | 155 ++++++++++++++++++++++++++++++ src/utils/serve-functions.js | 11 ++- 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/commands/functions/trigger.js diff --git a/src/commands/functions/trigger.js b/src/commands/functions/trigger.js new file mode 100644 index 0000000..2025345 --- /dev/null +++ b/src/commands/functions/trigger.js @@ -0,0 +1,155 @@ +const chalk = require("chalk"); +const Command = require("@netlify/cli-utils"); +const { flags } = require("@oclif/command"); +const inquirer = require("inquirer"); +// const AsciiTable = require("ascii-table"); +const { serverSettings } = require("../../detect-server"); +const fetch = require("node-fetch"); + +const { getFunctions } = require("../../utils/get-functions"); + +// https://www.netlify.com/docs/functions/#event-triggered-functions +const eventTriggeredFunctions = [ + "deploy-building", + "deploy-succeeded", + "deploy-failed", + "deploy-locked", + "deploy-unlocked", + "split-test-activated", + "split-test-deactivated", + "split-test-modified", + "submission-created", + "identity-validate", + "identity-signup", + "identity-login" +]; +class FunctionsTriggerCommand extends Command { + async run() { + let { flags } = this.parse(FunctionsTriggerCommand); + const { api, site, config } = this.netlify; + + const functionsDir = + flags.functions || + (config.dev && config.dev.functions) || + (config.build && config.build.functions); + if (typeof functionsDir === "undefined") { + this.error( + "functions directory is undefined, did you forget to set it in netlify.toml?" + ); + process.exit(1); + } + + let settings = await serverSettings(Object.assign({}, config.dev, flags)); + + if (!(settings && settings.command)) { + settings = { + noCmd: true, + port: 8888, + proxyPort: 3999, + dist + }; + } + + const functions = getFunctions(functionsDir); + let functionToTrigger = flags.name; + const isValidFn = Object.keys(functions).includes(functionToTrigger); + if (!functionToTrigger || !isValidFn) { + if (!isValidFn) { + this.warn( + `Invalid function name ${chalk.yellow(flags.name)} supplied as flag.` + ); + } + const { trigger } = await inquirer.prompt([ + { + type: "list", + message: "Pick a function to trigger", + name: "trigger", + choices: Object.keys(functions) + } + ]); + functionToTrigger = trigger; + } + let headers = {}; + let body = {}; + if (eventTriggeredFunctions.includes(functionToTrigger)) { + // https://www.netlify.com/docs/functions/#event-triggered-functions + const parts = functionToTrigger.split("-"); + if (parts[0] === "identity") { + // https://www.netlify.com/docs/functions/#identity-event-functions + body.event = parts[1]; + body.user = { + email: "foo@trust-this-company.com", + user_metadata: { + TODO: "mock our netliy identity user data better" + } + }; + } else { + // non identity functions seem to have a different shape + // https://www.netlify.com/docs/functions/#event-function-payloads + body.payload = { + TODO: "mock up payload data better" + }; + body.site = { + TODO: "mock up site data better" + }; + } + } else { + // NOT an event triggered function, but may still want to simulate authentication locally + const { isAuthed } = await inquirer.prompt([ + { + type: "confirm", + name: "isAuthed", + message: `Run with emulated Netlify Identity?`, + default: true + } + ]); + if (isAuthed) { + headers = { + authorization: + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c" + }; + // you can decode this https://jwt.io/ + // { + // "source": "netlify functions:trigger", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT" + // } + } + } + // fetch + fetch( + `http://localhost:${ + settings.port + }/.netlify/functions/${functionToTrigger}`, + { + method: "post", + headers, + body: JSON.stringify(body) + } + ) + .then(response => { + let data; + data = response.text(); + try { + // data = response.json(); + data = JSON.parse(data); + } catch (err) {} + return data; + }) + .then(console.log); + } +} + +FunctionsTriggerCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`; +FunctionsTriggerCommand.aliases = ["function:trigger"]; +FunctionsTriggerCommand.flags = { + name: flags.string({ char: "n", description: "function name to trigger" }), + functions: flags.string({ + char: "f", + description: "Specify a functions folder to serve" + }) +}; + +// TODO make visible once implementation complete +FunctionsTriggerCommand.hidden = false; + +module.exports = FunctionsTriggerCommand; diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index c7b13e6..8285119 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -43,8 +43,15 @@ function buildClientContext(headers) { try { return { identity: { - url: "NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_URL", - token: "NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_TOKEN" + url: "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY_URL", + token: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI" + // you can decode this with https://jwt.io/ + // just says + // { + // "source": "netlify dev", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY" + // } }, user: jwtDecode(parts[1]) }; From 6b2429ac1595deef3a214b3afd801658efc07194 Mon Sep 17 00:00:00 2001 From: sw-yx Date: Tue, 9 Jul 2019 14:35:08 -0400 Subject: [PATCH 2/2] flip to functionsinvoke --- src/commands/functions/invoke.js | 273 ++++++++++++++++++++++++++++++ src/commands/functions/trigger.js | 155 ----------------- src/utils/serve-functions.js | 3 +- 3 files changed, 275 insertions(+), 156 deletions(-) create mode 100644 src/commands/functions/invoke.js delete mode 100644 src/commands/functions/trigger.js diff --git a/src/commands/functions/invoke.js b/src/commands/functions/invoke.js new file mode 100644 index 0000000..7427518 --- /dev/null +++ b/src/commands/functions/invoke.js @@ -0,0 +1,273 @@ +const chalk = require("chalk"); +const Command = require("@netlify/cli-utils"); +const { flags } = require("@oclif/command"); +const inquirer = require("inquirer"); +const { serverSettings } = require("../../detect-server"); +const fetch = require("node-fetch"); +const fs = require("fs"); +const path = require("path"); + +const { getFunctions } = require("../../utils/get-functions"); + +// https://www.netlify.com/docs/functions/#event-triggered-functions +const eventTriggeredFunctions = [ + "deploy-building", + "deploy-succeeded", + "deploy-failed", + "deploy-locked", + "deploy-unlocked", + "split-test-activated", + "split-test-deactivated", + "split-test-modified", + "submission-created", + "identity-validate", + "identity-signup", + "identity-login" +]; +class FunctionsInvokeCommand extends Command { + async run() { + let { flags, args } = this.parse(FunctionsInvokeCommand); + const { api, site, config } = this.netlify; + + const functionsDir = + flags.functions || + (config.dev && config.dev.functions) || + (config.build && config.build.functions); + if (typeof functionsDir === "undefined") { + this.error( + "functions directory is undefined, did you forget to set it in netlify.toml?" + ); + process.exit(1); + } + + let settings = await serverSettings(Object.assign({}, config.dev, flags)); + + if (!(settings && settings.command)) { + settings = { + noCmd: true, + port: 8888, + proxyPort: 3999, + dist + }; + } + + const functions = getFunctions(functionsDir); + const functionToTrigger = await getNameFromArgs(functions, args, flags); + + let headers = {}; + let body = {}; + + if (eventTriggeredFunctions.includes(functionToTrigger)) { + /** handle event triggered fns */ + // https://www.netlify.com/docs/functions/#event-triggered-functions + const parts = functionToTrigger.split("-"); + if (parts[0] === "identity") { + // https://www.netlify.com/docs/functions/#identity-event-functions + body.event = parts[1]; + body.user = { + email: "foo@trust-this-company.com", + user_metadata: { + TODO: "mock our netlify identity user data better" + } + }; + } else { + // non identity functions seem to have a different shape + // https://www.netlify.com/docs/functions/#event-function-payloads + body.payload = { + TODO: "mock up payload data better" + }; + body.site = { + TODO: "mock up site data better" + }; + } + } else { + // NOT an event triggered function, but may still want to simulate authentication locally + let _isAuthed = false; + if (typeof flags.auth === "undefined") { + const { isAuthed } = await inquirer.prompt([ + { + type: "confirm", + name: "isAuthed", + message: `Invoke with emulated Netlify Identity authentication headers? (pass -auth/--no-auth to override)`, + default: true + } + ]); + _isAuthed = isAuthed; + } else { + _isAuthed = flags.auth; + } + if (_isAuthed) { + headers = { + authorization: + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c" + }; + // you can decode this https://jwt.io/ + // { + // "source": "netlify functions:trigger", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT" + // } + } + } + const payload = processPayloadFromFlag(flags.payload); + body = Object.assign({}, body, payload); + + // fetch + fetch( + `http://localhost:${ + settings.port + }/.netlify/functions/${functionToTrigger}` + + formatQstring(flags.querystring), + { + method: "post", + headers, + body: JSON.stringify(body) + } + ) + .then(response => { + let data; + data = response.text(); + try { + // data = response.json(); + data = JSON.parse(data); + } catch (err) {} + return data; + }) + .then(console.log) + .catch(err => { + console.error("ran into an error invoking your function"); + console.error(err); + }); + } +} + +function formatQstring(querystring) { + if (querystring) { + return "?" + querystring; + } else { + return ""; + } +} + +/** process payloads from flag */ +function processPayloadFromFlag(payloadString) { + if (payloadString) { + // case 1: jsonstring + let payload = tryParseJSON(payloadString); + if (!!payload) return payload; + // case 2: jsonpath + const payloadpath = path.join(process.cwd(), payloadString); + const pathexists = fs.existsSync(payloadpath); + if (!payload && pathexists) { + try { + payload = require(payloadpath); // there is code execution potential here + return payload; + } catch (err) { + console.error(err); + payload = false; + } + } + // case 3: invalid string, invalid path + return false; + } +} + +// prompt for a name if name not supplied +// also used in functions:create +async function getNameFromArgs(functions, args, flags) { + // let functionToTrigger = flags.name; + // const isValidFn = Object.keys(functions).includes(functionToTrigger); + if (flags.name && args.name) { + console.error( + "function name specified in both flag and arg format, pick one" + ); + process.exit(1); + } + let functionToTrigger; + if (flags.name && !args.name) functionToTrigger = flags.name; + // use flag if exists + else if (!flags.name && args.name) functionToTrigger = args.name; + + const isValidFn = Object.keys(functions).includes(functionToTrigger); + if (!functionToTrigger || !isValidFn) { + if (!isValidFn) { + this.warn( + `Function name ${chalk.yellow( + functionToTrigger + )} supplied but no matching function found in your functions folder, forcing you to pick a valid one...` + ); + } + const { trigger } = await inquirer.prompt([ + { + type: "list", + message: "Pick a function to trigger", + name: "trigger", + choices: Object.keys(functions) + } + ]); + functionToTrigger = trigger; + } + + return functionToTrigger; +} + +FunctionsInvokeCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`; +FunctionsInvokeCommand.aliases = ["function:trigger"]; + +FunctionsInvokeCommand.examples = [ + "$ netlify functions:invoke", + "$ netlify functions:invoke myfunction", + "$ netlify functions:invoke --name myfunction", + "$ netlify functions:invoke --name myfunction --auth", + "$ netlify functions:invoke --name myfunction --no-auth", + '$ netlify functions:invoke myfunction --payload "{"foo": 1}"', + '$ netlify functions:invoke myfunction --querystring "foo=1', + '$ netlify functions:invoke myfunction --payload "./pathTo.json"' +]; +FunctionsInvokeCommand.args = [ + { + name: "name", + description: "function name to invoke" + } +]; + +FunctionsInvokeCommand.flags = { + name: flags.string({ char: "n", description: "function name to invoke" }), + functions: flags.string({ + char: "f", + description: "Specify a functions folder to parse, overriding netlify.toml" + }), + querystring: flags.string({ + char: "q", + description: "Querystring to add to your function invocation" + }), + payload: flags.string({ + char: "p", + description: + "Supply POST payload in stringified json, or a path to a json file" + }), + auth: flags.boolean({ + char: "a", + description: + "simulate Netlify Identity authentication JWT. pass --no-auth to affirm unauthenticated request", + allowNo: true + }) +}; + +module.exports = FunctionsInvokeCommand; + +// https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try +function tryParseJSON(jsonString) { + try { + var o = JSON.parse(jsonString); + + // Handle non-exception-throwing cases: + // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking, + // but... JSON.parse(null) returns null, and typeof null === "object", + // so we must check for that, too. Thankfully, null is falsey, so this suffices: + if (o && typeof o === "object") { + return o; + } + } catch (e) {} + + return false; +} diff --git a/src/commands/functions/trigger.js b/src/commands/functions/trigger.js deleted file mode 100644 index 2025345..0000000 --- a/src/commands/functions/trigger.js +++ /dev/null @@ -1,155 +0,0 @@ -const chalk = require("chalk"); -const Command = require("@netlify/cli-utils"); -const { flags } = require("@oclif/command"); -const inquirer = require("inquirer"); -// const AsciiTable = require("ascii-table"); -const { serverSettings } = require("../../detect-server"); -const fetch = require("node-fetch"); - -const { getFunctions } = require("../../utils/get-functions"); - -// https://www.netlify.com/docs/functions/#event-triggered-functions -const eventTriggeredFunctions = [ - "deploy-building", - "deploy-succeeded", - "deploy-failed", - "deploy-locked", - "deploy-unlocked", - "split-test-activated", - "split-test-deactivated", - "split-test-modified", - "submission-created", - "identity-validate", - "identity-signup", - "identity-login" -]; -class FunctionsTriggerCommand extends Command { - async run() { - let { flags } = this.parse(FunctionsTriggerCommand); - const { api, site, config } = this.netlify; - - const functionsDir = - flags.functions || - (config.dev && config.dev.functions) || - (config.build && config.build.functions); - if (typeof functionsDir === "undefined") { - this.error( - "functions directory is undefined, did you forget to set it in netlify.toml?" - ); - process.exit(1); - } - - let settings = await serverSettings(Object.assign({}, config.dev, flags)); - - if (!(settings && settings.command)) { - settings = { - noCmd: true, - port: 8888, - proxyPort: 3999, - dist - }; - } - - const functions = getFunctions(functionsDir); - let functionToTrigger = flags.name; - const isValidFn = Object.keys(functions).includes(functionToTrigger); - if (!functionToTrigger || !isValidFn) { - if (!isValidFn) { - this.warn( - `Invalid function name ${chalk.yellow(flags.name)} supplied as flag.` - ); - } - const { trigger } = await inquirer.prompt([ - { - type: "list", - message: "Pick a function to trigger", - name: "trigger", - choices: Object.keys(functions) - } - ]); - functionToTrigger = trigger; - } - let headers = {}; - let body = {}; - if (eventTriggeredFunctions.includes(functionToTrigger)) { - // https://www.netlify.com/docs/functions/#event-triggered-functions - const parts = functionToTrigger.split("-"); - if (parts[0] === "identity") { - // https://www.netlify.com/docs/functions/#identity-event-functions - body.event = parts[1]; - body.user = { - email: "foo@trust-this-company.com", - user_metadata: { - TODO: "mock our netliy identity user data better" - } - }; - } else { - // non identity functions seem to have a different shape - // https://www.netlify.com/docs/functions/#event-function-payloads - body.payload = { - TODO: "mock up payload data better" - }; - body.site = { - TODO: "mock up site data better" - }; - } - } else { - // NOT an event triggered function, but may still want to simulate authentication locally - const { isAuthed } = await inquirer.prompt([ - { - type: "confirm", - name: "isAuthed", - message: `Run with emulated Netlify Identity?`, - default: true - } - ]); - if (isAuthed) { - headers = { - authorization: - "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c" - }; - // you can decode this https://jwt.io/ - // { - // "source": "netlify functions:trigger", - // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT" - // } - } - } - // fetch - fetch( - `http://localhost:${ - settings.port - }/.netlify/functions/${functionToTrigger}`, - { - method: "post", - headers, - body: JSON.stringify(body) - } - ) - .then(response => { - let data; - data = response.text(); - try { - // data = response.json(); - data = JSON.parse(data); - } catch (err) {} - return data; - }) - .then(console.log); - } -} - -FunctionsTriggerCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`; -FunctionsTriggerCommand.aliases = ["function:trigger"]; -FunctionsTriggerCommand.flags = { - name: flags.string({ char: "n", description: "function name to trigger" }), - functions: flags.string({ - char: "f", - description: "Specify a functions folder to serve" - }) -}; - -// TODO make visible once implementation complete -FunctionsTriggerCommand.hidden = false; - -module.exports = FunctionsTriggerCommand; diff --git a/src/utils/serve-functions.js b/src/utils/serve-functions.js index 8285119..dbbca38 100644 --- a/src/utils/serve-functions.js +++ b/src/utils/serve-functions.js @@ -43,7 +43,8 @@ function buildClientContext(headers) { try { return { identity: { - url: "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY_URL", + url: + "https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI" // you can decode this with https://jwt.io/