Skip to content
This repository was archived by the owner on Sep 12, 2019. It is now read-only.

Commit ef6ed43

Browse files
authored
[feature] add functions:invoke command (#213)
* add functions:trigger command * flip to functionsinvoke
1 parent f6ab964 commit ef6ed43

File tree

2 files changed

+283
-2
lines changed

2 files changed

+283
-2
lines changed

src/commands/functions/invoke.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
const chalk = require("chalk");
2+
const Command = require("@netlify/cli-utils");
3+
const { flags } = require("@oclif/command");
4+
const inquirer = require("inquirer");
5+
const { serverSettings } = require("../../detect-server");
6+
const fetch = require("node-fetch");
7+
const fs = require("fs");
8+
const path = require("path");
9+
10+
const { getFunctions } = require("../../utils/get-functions");
11+
12+
// https://www.netlify.com/docs/functions/#event-triggered-functions
13+
const eventTriggeredFunctions = [
14+
"deploy-building",
15+
"deploy-succeeded",
16+
"deploy-failed",
17+
"deploy-locked",
18+
"deploy-unlocked",
19+
"split-test-activated",
20+
"split-test-deactivated",
21+
"split-test-modified",
22+
"submission-created",
23+
"identity-validate",
24+
"identity-signup",
25+
"identity-login"
26+
];
27+
class FunctionsInvokeCommand extends Command {
28+
async run() {
29+
let { flags, args } = this.parse(FunctionsInvokeCommand);
30+
const { api, site, config } = this.netlify;
31+
32+
const functionsDir =
33+
flags.functions ||
34+
(config.dev && config.dev.functions) ||
35+
(config.build && config.build.functions);
36+
if (typeof functionsDir === "undefined") {
37+
this.error(
38+
"functions directory is undefined, did you forget to set it in netlify.toml?"
39+
);
40+
process.exit(1);
41+
}
42+
43+
let settings = await serverSettings(Object.assign({}, config.dev, flags));
44+
45+
if (!(settings && settings.command)) {
46+
settings = {
47+
noCmd: true,
48+
port: 8888,
49+
proxyPort: 3999,
50+
dist
51+
};
52+
}
53+
54+
const functions = getFunctions(functionsDir);
55+
const functionToTrigger = await getNameFromArgs(functions, args, flags);
56+
57+
let headers = {};
58+
let body = {};
59+
60+
if (eventTriggeredFunctions.includes(functionToTrigger)) {
61+
/** handle event triggered fns */
62+
// https://www.netlify.com/docs/functions/#event-triggered-functions
63+
const parts = functionToTrigger.split("-");
64+
if (parts[0] === "identity") {
65+
// https://www.netlify.com/docs/functions/#identity-event-functions
66+
body.event = parts[1];
67+
body.user = {
68+
email: "foo@trust-this-company.com",
69+
user_metadata: {
70+
TODO: "mock our netlify identity user data better"
71+
}
72+
};
73+
} else {
74+
// non identity functions seem to have a different shape
75+
// https://www.netlify.com/docs/functions/#event-function-payloads
76+
body.payload = {
77+
TODO: "mock up payload data better"
78+
};
79+
body.site = {
80+
TODO: "mock up site data better"
81+
};
82+
}
83+
} else {
84+
// NOT an event triggered function, but may still want to simulate authentication locally
85+
let _isAuthed = false;
86+
if (typeof flags.auth === "undefined") {
87+
const { isAuthed } = await inquirer.prompt([
88+
{
89+
type: "confirm",
90+
name: "isAuthed",
91+
message: `Invoke with emulated Netlify Identity authentication headers? (pass -auth/--no-auth to override)`,
92+
default: true
93+
}
94+
]);
95+
_isAuthed = isAuthed;
96+
} else {
97+
_isAuthed = flags.auth;
98+
}
99+
if (_isAuthed) {
100+
headers = {
101+
authorization:
102+
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c"
103+
};
104+
// you can decode this https://jwt.io/
105+
// {
106+
// "source": "netlify functions:trigger",
107+
// "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT"
108+
// }
109+
}
110+
}
111+
const payload = processPayloadFromFlag(flags.payload);
112+
body = Object.assign({}, body, payload);
113+
114+
// fetch
115+
fetch(
116+
`http://localhost:${
117+
settings.port
118+
}/.netlify/functions/${functionToTrigger}` +
119+
formatQstring(flags.querystring),
120+
{
121+
method: "post",
122+
headers,
123+
body: JSON.stringify(body)
124+
}
125+
)
126+
.then(response => {
127+
let data;
128+
data = response.text();
129+
try {
130+
// data = response.json();
131+
data = JSON.parse(data);
132+
} catch (err) {}
133+
return data;
134+
})
135+
.then(console.log)
136+
.catch(err => {
137+
console.error("ran into an error invoking your function");
138+
console.error(err);
139+
});
140+
}
141+
}
142+
143+
function formatQstring(querystring) {
144+
if (querystring) {
145+
return "?" + querystring;
146+
} else {
147+
return "";
148+
}
149+
}
150+
151+
/** process payloads from flag */
152+
function processPayloadFromFlag(payloadString) {
153+
if (payloadString) {
154+
// case 1: jsonstring
155+
let payload = tryParseJSON(payloadString);
156+
if (!!payload) return payload;
157+
// case 2: jsonpath
158+
const payloadpath = path.join(process.cwd(), payloadString);
159+
const pathexists = fs.existsSync(payloadpath);
160+
if (!payload && pathexists) {
161+
try {
162+
payload = require(payloadpath); // there is code execution potential here
163+
return payload;
164+
} catch (err) {
165+
console.error(err);
166+
payload = false;
167+
}
168+
}
169+
// case 3: invalid string, invalid path
170+
return false;
171+
}
172+
}
173+
174+
// prompt for a name if name not supplied
175+
// also used in functions:create
176+
async function getNameFromArgs(functions, args, flags) {
177+
// let functionToTrigger = flags.name;
178+
// const isValidFn = Object.keys(functions).includes(functionToTrigger);
179+
if (flags.name && args.name) {
180+
console.error(
181+
"function name specified in both flag and arg format, pick one"
182+
);
183+
process.exit(1);
184+
}
185+
let functionToTrigger;
186+
if (flags.name && !args.name) functionToTrigger = flags.name;
187+
// use flag if exists
188+
else if (!flags.name && args.name) functionToTrigger = args.name;
189+
190+
const isValidFn = Object.keys(functions).includes(functionToTrigger);
191+
if (!functionToTrigger || !isValidFn) {
192+
if (!isValidFn) {
193+
this.warn(
194+
`Function name ${chalk.yellow(
195+
functionToTrigger
196+
)} supplied but no matching function found in your functions folder, forcing you to pick a valid one...`
197+
);
198+
}
199+
const { trigger } = await inquirer.prompt([
200+
{
201+
type: "list",
202+
message: "Pick a function to trigger",
203+
name: "trigger",
204+
choices: Object.keys(functions)
205+
}
206+
]);
207+
functionToTrigger = trigger;
208+
}
209+
210+
return functionToTrigger;
211+
}
212+
213+
FunctionsInvokeCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`;
214+
FunctionsInvokeCommand.aliases = ["function:trigger"];
215+
216+
FunctionsInvokeCommand.examples = [
217+
"$ netlify functions:invoke",
218+
"$ netlify functions:invoke myfunction",
219+
"$ netlify functions:invoke --name myfunction",
220+
"$ netlify functions:invoke --name myfunction --auth",
221+
"$ netlify functions:invoke --name myfunction --no-auth",
222+
'$ netlify functions:invoke myfunction --payload "{"foo": 1}"',
223+
'$ netlify functions:invoke myfunction --querystring "foo=1',
224+
'$ netlify functions:invoke myfunction --payload "./pathTo.json"'
225+
];
226+
FunctionsInvokeCommand.args = [
227+
{
228+
name: "name",
229+
description: "function name to invoke"
230+
}
231+
];
232+
233+
FunctionsInvokeCommand.flags = {
234+
name: flags.string({ char: "n", description: "function name to invoke" }),
235+
functions: flags.string({
236+
char: "f",
237+
description: "Specify a functions folder to parse, overriding netlify.toml"
238+
}),
239+
querystring: flags.string({
240+
char: "q",
241+
description: "Querystring to add to your function invocation"
242+
}),
243+
payload: flags.string({
244+
char: "p",
245+
description:
246+
"Supply POST payload in stringified json, or a path to a json file"
247+
}),
248+
auth: flags.boolean({
249+
char: "a",
250+
description:
251+
"simulate Netlify Identity authentication JWT. pass --no-auth to affirm unauthenticated request",
252+
allowNo: true
253+
})
254+
};
255+
256+
module.exports = FunctionsInvokeCommand;
257+
258+
// https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try
259+
function tryParseJSON(jsonString) {
260+
try {
261+
var o = JSON.parse(jsonString);
262+
263+
// Handle non-exception-throwing cases:
264+
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
265+
// but... JSON.parse(null) returns null, and typeof null === "object",
266+
// so we must check for that, too. Thankfully, null is falsey, so this suffices:
267+
if (o && typeof o === "object") {
268+
return o;
269+
}
270+
} catch (e) {}
271+
272+
return false;
273+
}

src/utils/serve-functions.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,16 @@ function buildClientContext(headers) {
4343
try {
4444
return {
4545
identity: {
46-
url: "NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_URL",
47-
token: "NETLIFY_LAMBDA_LOCALLY_EMULATED_IDENTITY_TOKEN"
46+
url:
47+
"https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity",
48+
token:
49+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI"
50+
// you can decode this with https://jwt.io/
51+
// just says
52+
// {
53+
// "source": "netlify dev",
54+
// "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY"
55+
// }
4856
},
4957
user: jwtDecode(parts[1])
5058
};

0 commit comments

Comments
 (0)