netlify-cli
Version:
Netlify command line tool
290 lines (260 loc) • 9 kB
JavaScript
const fs = require('fs')
const path = require('path')
const process = require('process')
const { flags: flagsLib } = require('@oclif/command')
const chalk = require('chalk')
const inquirer = require('inquirer')
const fetch = require('node-fetch')
const Command = require('../../utils/command')
const { getFunctions } = require('../../utils/get-functions')
const { NETLIFYDEVWARN } = require('../../utils/logo')
// https://www.netlify.com/docs/functions/#event-triggered-functions
const eventTriggeredFunctions = new Set([
'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',
])
const DEFAULT_PORT = 8888
class FunctionsInvokeCommand extends Command {
async run() {
const { flags, args } = this.parse(FunctionsInvokeCommand)
const { 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)
}
if (!flags.port)
console.warn(
`${NETLIFYDEVWARN} "port" flag was not specified. Attempting to connect to localhost:8888 by default`,
)
const port = flags.port || DEFAULT_PORT
const functions = await getFunctions(functionsDir)
const functionToTrigger = await getNameFromArgs(functions, args, flags)
let headers = {}
let body = {}
await this.config.runHook('analytics', {
eventName: 'command',
payload: {
command: 'functions:invoke',
},
})
if (eventTriggeredFunctions.has(functionToTrigger)) {
/** handle event triggered fns */
// https://www.netlify.com/docs/functions/#event-triggered-functions
const [name, event] = functionToTrigger.split('-')
if (name === 'identity') {
// https://www.netlify.com/docs/functions/#identity-event-functions
body.event = event
body.user = {
id: '1111a1a1-a11a-1111-aa11-aaa11111a11a',
aud: '',
role: '',
email: 'foo@trust-this-company.com',
app_metadata: {
provider: 'email',
},
user_metadata: {
full_name: 'Test Person',
},
created_at: new Date(Date.now()).toISOString(),
update_at: new Date(Date.now()).toISOString(),
}
} 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 isAuthenticated = false
if (typeof flags.identity === 'undefined') {
const { isAuthed } = await inquirer.prompt([
{
type: 'confirm',
name: 'isAuthed',
message: `Invoke with emulated Netlify Identity authentication headers? (pass --identity/--no-identity to override)`,
default: true,
},
])
isAuthenticated = isAuthed
} else {
isAuthenticated = flags.identity
}
if (isAuthenticated) {
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 = { ...body, ...payload }
// fetch
fetch(`http://localhost:${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 (error) {}
return data
})
.then(console.log)
.catch((error) => {
console.error('ran into an error invoking your function')
console.error(error)
})
}
}
const formatQstring = function (querystring) {
if (querystring) {
return `?${querystring}`
}
return ''
}
/** process payloads from flag */
const processPayloadFromFlag = function (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 (pathexists) {
try {
// there is code execution potential here
// eslint-disable-next-line node/global-require, import/no-dynamic-require
payload = require(payloadpath)
return payload
} catch (error) {
console.error(error)
}
}
// case 3: invalid string, invalid path
return false
}
}
// prompt for a name if name not supplied
// also used in functions:create
const getNameFromArgs = async function (functions, args, flags) {
const functionToTrigger = getFunctionToTrigger(args, flags)
const functionNames = functions.map(getFunctionName)
if (functionToTrigger) {
if (functionNames.includes(functionToTrigger)) {
return functionToTrigger
}
console.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: functionNames,
},
])
return trigger
}
const getFunctionToTrigger = function (args, flags) {
if (flags.name) {
if (args.name) {
console.error('function name specified in both flag and arg format, pick one')
process.exit(1)
}
return flags.name
}
return args.name
}
const getFunctionName = function ({ name }) {
return name
}
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 --identity',
'$ netlify functions:invoke --name myfunction --no-identity',
`$ 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: flagsLib.string({
char: 'n',
description: 'function name to invoke',
}),
functions: flagsLib.string({
char: 'f',
description: 'Specify a functions folder to parse, overriding netlify.toml',
}),
querystring: flagsLib.string({
char: 'q',
description: 'Querystring to add to your function invocation',
}),
payload: flagsLib.string({
char: 'p',
description: 'Supply POST payload in stringified json, or a path to a json file',
}),
identity: flagsLib.boolean({
description: 'simulate Netlify Identity authentication JWT. pass --no-identity to affirm unauthenticated request',
allowNo: true,
}),
port: flagsLib.integer({
description: 'Port where netlify dev is accessible. e.g. 8888',
}),
...FunctionsInvokeCommand.flags,
}
module.exports = FunctionsInvokeCommand
// https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try
const tryParseJSON = function (jsonString) {
try {
const parsedValue = 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 (parsedValue && typeof parsedValue === 'object') {
return parsedValue
}
} catch (error) {}
return false
}