How to write fetch in "Code by Zapier"? - node.js

In zapier I use an action of Code By Zapier. It's based on node.js.
I need to use fetch for implementing REST-API of my CRM.
Here is the code I wrote, which runs well when I tried it with VS Code (outside Zapier):
// the code by zapier includes already the require('fetch')
var api_token = "..."; // my api
var deal_name = "Example"; // a string
fetch("https://api.pipedrive.com/v1/deals/find?term="+deal_name+"&api_token=" + api_token)
.then(function(res) {
return res.json();
}).then(function(json) {
var deal_id = json.data[0].id;
console.log("deal_id="+deal_id);
}).catch(function(error) {
console.log("error");
});
output = {id: 1, hello: "world"}; // must include output...
The error I got from Zapier is:
If you are doing async (with fetch library) you need to use a
callback!
Please help me with solving it.

This is a classic mistake when coding in Node.js/callback environments.
You are using console.log which prints to your console, but doesn't return data to the parent (Zapier in this case).
Here is an example of bad and good code:
// bad code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// when i run this in my node repl it works perfect!
// the problem is this doesn't return the data to zapier
// it just prints it to the system output
console.log(json);
});
// good code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// but if i swap this to callback, this works perfect in zapier
callback(null, json);
});
I hope this helps!

These days, you can also use async/await, as noted by the default comment at the top of the sample code block:
// this is wrapped in an `async` function
// you can use await throughout the function
const response = await fetch('http://worldclockapi.com/api/json/utc/now')
return await response.json()
See further examples in the docs: https://zapier.com/help/create/code-webhooks/javascript-code-examples-in-zaps#step-2
Note that the free-tier has a 1 second timeout (especially relevant if you use Promise.all() to execute multiple fetches!)

Related

Node.js concurrent API GETs, return as soon as one responses

I have a list of APIs I want to call GET simultaneously on all of them and return as soon as one API finishes the request with a response code of 200.
I tried using a for-loop and break, but that doesn't seem to work. It would always use the first API
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
let response;
for (const api of listOfApi) {
try {
response = await axios.get(api, {
data: {
url: 'https://example.com/',
},
});
break;
} catch (error) {
console.error(`Error occurred: ${error.message}`);
}
}
You can use Promise.race() to see which of an array of promises finishes first while running all the requests in parallel in flight at the same time:
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
Promise.any(listOfApi.map(api => {
return axios.get(api, {data: {url: 'https://example.com/'}}).then(response => {
// skip any responses without a status of 200
if (response.status !== 200) {
throw new Error(`Response status ${response.status}`, {cause: response});
}
return response;
});
})).then(result => {
// first result available here
console.log(result);
}).catch(err => {
console.log(err);
});
Note, this uses Promise.any() which finds the first promise that resolves successfully (skipping promises that reject). You can also use Promise.race() if you want the first promise that resolves or rejects.
I think jfriend00's answer is good, but I want to expand on it a bit and show how it would look with async/await, because that's what you are already using.
As mentioned, you can use Promise.any (or Promise.race). Both take an array of promises as argument. Promise.any will yield the result of the first promise that resolves successfully, while Promise.race will simply wait for the first promise that finishes (regardless of whether it was fulfilled or rejected) and yield its result.
To keep your code in the style of async/await as it originally was, you can map the array using an async callback function, which will effectively return a promise. This way, you don't have to "branch off into .then territory" and can keep the code more readable and easier to expand with conditions, etc.
This way, the code can look as follows:
import axios from 'axios';
const listOfApi = ['https://example.com/api/instanceOne', 'https://example.com/api/instanceTwo'];
try {
const firstResponse = await Promise.any(listOfApi.map(async api => {
const response = await axios.get(api, {
data: {
url: 'https://example.com/',
},
});
if (response.status !== 200) {
throw new Error(`Response status ${response.status}`, {cause: response});
}
return response;
}));
// DO SOMETHING WITH firstResponse HERE
} catch (error) {
console.error('Error occured:', error);
}
Side note: I changed your console.error slightly. Logging only error.message is a common mistake that hinders you from effective debugging later on, because it will lack a lot of important information because it prints only the message and not the error stack, the error name or any additional properties the error may have. Using .stack and not .message will already be better as it includes name and stack then, but what's best is to supply the error as separate argument to console.error so that inspect gets called on it and it can print the whole error object, with stack and any additional properties you may be interested in. This is very valuable when you encounter an error in production that is not so easy to reproduce.

Keep variable between promises in a function?

On firebase function I need to get data from Paypal and do 4 things :
1. returns an empty HTTP 200 to them.
2. send the complete message back to PayPal using `HTTPS POST`.
3. get back "VERIFIED" message from Paypal.
4. *** write something to my Firebase database only here.
What I do now works but i am having a problem with (4).
exports.contentServer = functions.https.onRequest((request, response) => {
....
let options = {
method: 'POST',
uri: "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr",
body: verificationBody
};
// ** say 200 to paypal
response.status(200).end();
// ** send POST to paypal back using npm request-promise
return rp(options).then(body => {
if (body === "VERIFIED") {
//*** problem is here!
return admin.firestore().collection('Users').add({request.body}).then(writeResult => {return console.log("Request completed");});
}
return console.log("Request completed");
})
.catch(error => {
return console.log(error);
})
As you can see when I get final VERIFIED from Paypal I try to write to the db with admin.firestore().collection('Users')..
I get a warning on compile :
Avoid nesting promises
for the write line.
How and where should I put this write at that stage of the promise ?
I understand that this HTTPS Cloud Function is called from Paypal.
By doing response.status(200).end(); at the beginning of your HTTP Cloud Function you are terminating it, as explained in the doc:
Important: Make sure that all HTTP functions terminate properly. By
terminating functions correctly, you can avoid excessive charges from
functions that run for too long. Terminate HTTP functions with
res.redirect(), res.send(), or res.end().
This means that in most cases the rest of the code will not be executed at all or the function will be terminated in the middle of the asynchronous work (i.e. the rp() or the add() methods)
You should send the response to the caller only when all the asynchronous work is finished. The following should work:
exports.contentServer = functions.https.onRequest((request, response) => {
let options = {
method: 'POST',
uri: "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr",
body: verificationBody
};
// ** send POST to paypal back using npm request-promise
return rp(options)
.then(body => {
if (body === "VERIFIED") {
//*** problem is here!
return admin.firestore().collection('Users').add({ body: request.body });
} else {
console.log("Body is not verified");
throw new Error("Body is not verified");
}
})
.then(docReference => {
console.log("Request completed");
response.send({ result: 'ok' }); //Or any other object, or empty
})
.catch(error => {
console.log(error);
response.status(500).send(error);
});
});
I would suggest you watch the official Video Series on Cloud Functions from Doug Stevenson (https://firebase.google.com/docs/functions/video-series/) and in particular the first video on Promises titled "Learn JavaScript Promises (Pt.1) with HTTP Triggers in Cloud Functions".

Making a request before all tests start in Mocha

I would like to test my simple API that has /groups URL.
I want to make an API request to that URL (using Axios) before all tests begin and make the response visible to all test functions.
I am trying to make the response visible but not able to make it work. I followed a similar case with filling out the DB upfront but no luck with my case.
My simple test file below:
var expect = require('chai').expect
var axios = require('axios')
var response = {};
describe('Categories', function() {
describe('Groups', function() {
before(function() {
axios.get(config.hostname + '/groups').then(function (response) {
return response;
})
});
it('returns a not empty set of results', function(done) {
expect(response).to.have.length.greaterThan(0);
done();
})
});
});
I tried also a sligh modification of before function:
before(function(done) {
axios.get(config.hostname + '/groups')
.then(function (response) {
return response;
}).then(function() {
done();
})
});
but no luck too.
The error I am getting is simply that response isn't changing nor is visible within it. AssertionError: expected {} to have property 'length'
Summarising: How can I pass response from axios inside to in()?
Your first form is incorrect, because you're not returning the chained promise. As such, mocha has no way of knowing when your before is finished, or even that it's async at all. Your second form will solve this problem, but since axios.get already returns a promise, it's kind of a waste not to use mocha's built-in promise support.
As for making the response visible in the it, you need to assign it to a variable in a scope that will be visible within the it.
var expect = require('chai').expect
var axios = require('axios')
var response;
describe('Categories', function() {
describe('Groups', function() {
before(function() {
// Note that I'm returning the chained promise here, as discussed.
return axios.get(config.hostname + '/groups').then(function (res) {
// Here's the assignment you need.
response = res;
})
});
// This test does not need the `done` because it is not asynchronous.
// It will not run until the promise returned in `before` resolves.
it('returns a not empty set of results', function() {
expect(response).to.have.length.greaterThan(0);
})
});
});

Sinon Fake XML Not Capturing Requests

I'm trying to write some tests using Lab and Sinon for various HTTP requests that are called in a file of mine. I followed the Fake XMLHttpRequest example at http://sinonjs.org/ but when I run my tests it appears to not actually capture any requests.
Here is the (relevant) testing code:
context('when provided a valid payload', function () {
let xhr;
let requests;
before(function (done) {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = function (req) { requests.push(req); };
done();
});
after(function (done) {
// clean up globals
xhr.restore();
done();
});
it('responds with the ticket id', (done) => {
create(internals.validOptions, sinon.spy());
console.log(requests); // Logs empty array []
done();
});
});
create is the function I imported from the other file, here:
internals.create = async function (request, reply) {
const engineeringTicket = request.payload.type === 'engineering';
const urgentTicket = request.payload.urgency === 'urgent';
if (validation.isValid(request.payload)) {
const attachmentPaths = formatUploads(request.payload.attachments);
const ticketData = await getTicket(request.payload, attachmentPaths);
if (engineeringTicket) {
const issueData = getIssue(request.payload);
const response = await jira.createIssue(issueData);
jira.addAttachment(response.id, attachmentPaths);
if (urgentTicket) {
const message = slack.getMessage(response);
slack.postToSlack(message);
}
}
zendesk.submitTicket(ticketData, function (error, statusCode, result) {
if (!error) {
reply(result).code(statusCode);
} else {
console.log(error);
}
});
} else {
reply({ errors: validation.errors }).code(400); // wrap in Boom
}
};
as you can see it calls jira.createIssue and zendesk.submitTicket, both of which use an HTTP request to post some payload to an API. However, after running the test, the requests variable is still empty and seems to have captured no requests. It is definitely not actually submitting the requests as no tickets/issues have been created, what do I need to fix to actually capture the requests?
Your problem is apparent from the tags: you are running the code in NodeJS, but the networking stubs in Sinon is for XMLHttpRequest, which is a browser specific API. It does not exist in Node, and as such, the setup will never work.
That means if this should have worked you would have needed to run the tests in a browser. The Karma test runner can help you with this if you need to automate it.
To make this work in Node you can either go for an approach where you try to stub out at a higher level - meaning stubbing the methods of zendesk and jira, or you can continue with the approach of stubbing network responses (which makes the tests a bit more brittle).
To continue stubbing out HTTP calls, you can do this in Node using Nock. Saving the requests like you did above is done like this:
var requests = [];
var scope = nock('http://www.google.com')
.get('/cat-poems')
.reply(function(uri, requestBody) {
requests.push( {uri, requestBody} );
});
To get some insights on how you can stub out at a higher level, I wrote this answer on using dependency injection and Sinon, while this article by Morgan Roderick gives an intro to link seams.

How to write node-fetch (Rest-API) in "Code by Zapier"?

In zapier I use an action of Code By Zapier. It's based on node.js. I need to use fetch for implementing REST-API of my CRM.
Here is the code I wrote, which runs well when I tried it with VS Code (outside Zapier):
// the code by zapier includes already the require('fetch')
var api_token = "..."; // my api
var deal_name = "Example"; // a string
fetch("https://api.pipedrive.com/v1/deals/find?term="+deal_name+"&api_token=" + api_token)
.then(function(res) {
return res.json();
}).then(function(json) {
var deal_id = json.data[0].id;
console.log("deal_id="+deal_id);
}).catch(function(error) {
console.log("error");
});
output = {id: 1, hello: "world"}; // must include output...
The error I got from Zapier is:
If you are doing async (with fetch library) you need to use a
callback!
Please help me with solving it.
Zapier knows, that fetch is a async function. You have to use the callback-function and not the output variable.
// bad code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// when i run this in my node repl it works perfect!
// the problem is this doesn't return the data to zapier
// it just prints it to the system output
console.log(json);
});
// good code
fetch(url)
.then(function(res) {
return res.json();
}).then(function(json) {
// but if i swap this to callback, this works perfect in zapier
callback(null, json);
});

Resources