0

I need to call an API recursively using request promise after getting result from API need to write in an excel file , API sample response given below

{
"totalRecords": 9524,
"size": 20,
"currentPage": 1,
"totalPages": 477,
"result": [{
        "name": "john doe",
        "dob": "1999-11-11"
    },
    {
        "name": "john1 doe1",
        "dob": "1989-12-12"
    }

]
}

Now I want to call this API n times, here n is equal to totalPages, after calling each API I want to write response result to the excel files. First write page 1 response result to excel then append page 2 response result to excel file and so on.. I have written some sample code given below

function callAPI(pageNo) {
var options = {
  url: "http://example.com/getData?pageNo="+pageNo,
  method: 'GET',
  headers: {        
    'Content-Type': 'application/json'
  },
  json: true
}   
return request(options)
}

callAPI(1).then(function (res) {
   // Write res.result to excel file      
}).catch(function (err) {
// Handle error here
})

But facing problem calling recursively API and maintaining sequentially like write page 1 result first to excel file then page 2 result append to excel and so on.. Any code sample how to achieve in nodejs

Dev
  • 413
  • 10
  • 27
  • My answer below assumes you want to request page 2 after requesting page1, but if you want to request them all at the same time I would offer a different answer – TKoL Feb 15 '18 at 17:47

3 Answers3

1

You want to do something like this:

function getAllPages() {
    function getNextPage(pageNo) {
        return callAPI(pageNo).then(response => {
            let needNextPage = true;
            if (pageNo === 1) {
                // write to file
            } else {
                // append to file
            }

            if (needNextPage) {
                return getNextPage(pageNo+1);
            } else {
                return undefined;
            }
        });
    }

    return getNextPage(1);
}

Obviously change that 'needNextPage' to false to stop the recursion when you're done

TKoL
  • 13,158
  • 3
  • 39
  • 73
  • if you need write/append to file to be waited for in the promise flow, then RETURN the write/append operation promise and put the `if needNextPage` block in the next 'then' – TKoL Feb 15 '18 at 16:16
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Feb 15 '18 at 17:36
  • @Bergi I disagree. This is not an example of the antipattern, because this is not an example where you can simply return a promise chain. If you believe there is a way to return a promise chain rather than making a promise and resolving an inner promise when the recursion is complete, I'd like to see how you did it. – TKoL Feb 15 '18 at 17:41
  • Just have `getNextPage` return the promise? Basically just remove the `new Promise(function …` wrapper and add three `return` tokens (one for every function) – Bergi Feb 15 '18 at 17:43
  • Can you post what you mean inside a jsfiddle? @Bergi – TKoL Feb 15 '18 at 17:44
  • I took the liberty to just edit your answer. Rollback if you don't like it. – Bergi Feb 15 '18 at 17:47
  • That does not work. I will post a jsfiddle for you to try, that demonstrates exactly why it doesn't work. – TKoL Feb 15 '18 at 17:52
  • 1
    @Bergi it does work and I've never seen it done this way before. This is obviously the correct way to do it, I'll keep your revisions. – TKoL Feb 15 '18 at 17:57
  • @Bergi I am considering the possibility that my original answer is more easy to understand, though, despite including an unnecessary wrapper promise. – TKoL Feb 16 '18 at 09:07
  • No, once you understood `then` it isn't. And the antipattern has many other drawbacks. – Bergi Feb 16 '18 at 15:35
  • I think the explicit "resolve" when you're ready to return everything helps me keep track of what's actually happening mentally, is what I mean. – TKoL Feb 16 '18 at 15:45
  • Maybe it would help to rename `getNextPage` to `getPagesFrom` – Bergi Feb 16 '18 at 15:46
  • @TKoL I want to put getAllPages() inside promise like `getAllPages().then(response => { console.log('All done') })` then what is the exact way to modify getAllPages I want to put resolve inside `else { resolve(1) return undefined; }` – Dev Feb 23 '18 at 12:24
1

So you want to do 477 requests in sequence? How long do you wanna wait for this to finish? Even in paralell, this would be still too long for me.

Best: write an API that can return you a batch of pages at once. Reducing the number of requests to the backend. Maybe something like http://example.com/getData?pages=1-100 and let it return an Array; maybe like

[
  {
    "totalRecords": 9524,
    "currentPage": 1,
    "totalPages": 477,
    "result": [...]
  },

  {
    "totalRecords": 9524,
    "currentPage": 2,
    "totalPages": 477,
    "result": [...]
  },
  ...
]

or more compact

{
  "totalRecords": 9524,
  "totalPages": 477,
  "pages": [
    {
      "currentPage": 1,
      "result": [...]
    },

    {
      "currentPage": 2,
      "result": [...]
    },
    ...
  ]
}

Sidenote: writing the size of the results array into the json is unnecessary. This value can easily be determined from data.result.length


But back to your question

Imo. all you want to run in sequence is adding the pages to the sheet. The requests can be done in paralell. That already saves you a lot of overall runtime for the whole task.

callApi(1).then(firstPage => {
  let {currentPage, totalPages} = firstPage;

  //`previous` ensures that the Promises resolve in sequence, 
  //even if some later request finish sooner that earlier ones.
  let previous = Promise.resolve(firstPage).then(writePageToExcel);

  while(++currentPage <= totalPages){
    //make the next request in paralell
    let p = callApi(currentPage);

    //execute `writePageToExcel` in sequence
    //as soon as all previous ones have finished
    previous = previous.then(() => p.then(writePageToExcel));
  }
  return previous;
})
.then(() => console.log("work done"));

or you wait for all pages to be loaded, before you write them to excel

callApi(1).then(firstPage => {
  let {currentPage, totalPages} = firstPage;

  let promises = [firstPage];
  while(++currentPage < totalPages)
    promises.push(callApi(currentPage));

  //wait for all requests to finish
  return Promise.all(promises);
})
//write all pages to excel
.then(writePagesToExcel)
.then(() => console.log("work done"));

or you could batch the requests

callApi(1).then(firstPage => {
  const batchSize = 16;
  let {currentPage, totalPages} = firstPage;

  return Promise.resolve([ firstPage ])
    .then(writePagesToExcel)
    .then(function nextBatch(){
      if(currentPage > totalPages) return;

      //load a batch of pages in paralell
      let batch = [];
      for(let i=0; i<batchSize && ++currentPage <= totalPages; ++i){
        batch[i] = callApi(currentPage);
      }

      //when the batch is done ...
      return Promise.all(batch)
        //... write it to the excel sheet ...
        .then(writePagesToExcel)
        //... and process the next batch
        .then(nextBatch);
    });
})
.then(() => console.log("work done"));

But don't forget to add the error handling. Since I'm not sure how you'd want to handle errors with the approaches I've posted, I didn't include the error-handling here.


Edit:

can u pls modify batch requests, getting some error, where you are assigning toalPages it's not right why the totalPages should equal to firstPage

let {currentPage, totalPages} = firstPage;
//is just a shorthand for
let currentPage = firstPage.currentPage, totalPages = firstPage.totalPages;
//what JS version are you targeting?

This first request, callApi(1).then(firstPage => ...) is primarily to determine currentIndex and totalLength, as you provide these properties in the returned JSON. Now that I know these two, I can initiate as many requests in paralell, as I'd want to. And I don't have to wait for any one of them to finish to determine at what index I am, and wether there are more pages to load.

and why you are writing return Promise.resolve([ firstPage ])

To save me some trouble and checking, as I don't know anything about how you'd implement writePagesToExcel.
I return Promise.resolve(...) so I can do .then(writePagesToExcel). This solves me two problems:

  1. I don't have to care wether writePagesToExcel returns sync or a promise and I can always follow up with another .then(...)

  2. I don't need to care wether writePagesToExcel may throw. In case of any Error, it all ends up in the Promise chain, and can be taken care of there.

So ultimately I safe myself a few checks, by simply wrapping firstPage back up in a Promise and continue with .then(...). Considering the amounts of data you're processing here, imo. this ain't too much of an overhead to get rid of some potential pitfalls.

why you are passing array like in resolve

To stay consistent in each example. In this example, I named the function that processes the data writePagesToExcel (plural) wich should indicate that it deals with multiple pages (an array of them); I thought that this would be clear in that context.

Since I still need this seperate call at the beginning to get firstPage, and I didn't want to complicate the logic in nextBatch just to concat this first page with the first batch, I treat [firstPage] as a seperate "batch", write it to excel and continue with nextBatch

Thomas
  • 11,958
  • 1
  • 14
  • 23
  • You are suggesting to fetch all API records at one call , if my API returns 60000 records will it not throw memory error. – Dev Feb 19 '18 at 13:40
  • @JohneDoe, your initial JSON said `"totalRecords": 9524` but, well OK. Did you check how much memory we're talking about? With the structure from your question and 60000 results, I'd guess something around 6-10MB. If that has to run on a mobile device, this might be an issue. On a Desktop computer, probably not. And even if you don't load all pages in one request, you should consider downloading something like 10 or 20 pages at once (in a single request) and therefore reduce the number of requests by that amount. It's always a matter of tradeoffs. – Thomas Feb 19 '18 at 14:22
  • can u pls modify batch requests, getting some error, where you are assigning toalPages it's not right why the totalPages should equal to firstPage – Dev Feb 19 '18 at 16:18
  • and why you are writing return Promise.resolve([ firstPage ]) why you are passing array like in resolve – Dev Feb 19 '18 at 16:23
  • @JohneDoe, I've updated the answer to address your questions – Thomas Feb 19 '18 at 17:16
  • Thanks, I'm implementing first one like writing below code for excel writing ` workbook.xlsx.writeFile('new.xlsx').then(function() { }).catch(function (err) { })` but if any excel write throw errors I want to stop apiCall execution immediate and want to catch this at final end like we have got `.then(() => console.log("work done"))` similarly we want to print `.catch(()=>console.log('error')` how to do this – Dev Feb 20 '18 at 17:48
-1
function callAPI(pageNo) {
  var options = {
    url: "http://example.com/getData?pageNo="+pageNo,
    method: 'GET',
    headers: {        
      'Content-Type': 'application/json'
    },
    json: true
  }   
  return request(options)
}

function writeToExcel(res){console.log(res)} //returns promise.


callAPI(1).then(function (res) {
   if(res){
       writeToExcel(res).then(() => {
          var emptyPromise = new Promise(res => setTimeout(res, 0));
          while(res && res.currentPage < res.totalPages){
             emptyPromise = emptyPromise.then(() => {
               return callAPI(res.currentPage).then(function (res){
                   if(res){
                     writeToExcel(res)
                   }
               });
             }
          }
          return emptyPromise; 
       });
   }
}).catch(function (err) {
// Handle error here
})
Aditya
  • 861
  • 5
  • 8
  • This will not work and fundamentally misunderstands async program flow, with that 'while' block. – TKoL Feb 15 '18 at 16:25
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Feb 15 '18 at 17:36
  • @TKoL why wouldn't it work. And I believe this is still async considering callAPI returns a promise – Aditya Feb 16 '18 at 06:50
  • @Bergi and from that post I couldn't infer how this is promise constructor antipattern. Empty promise here has been used to chain subsequent promises – Aditya Feb 16 '18 at 06:53
  • @Aditya because the while loop doesn't work in an async way. I'm not saying "this isn't async", I'm saying "this doesn't work properly with async". Some elements don't even make sense. `setTimeout(res, 0)` for example, and again, the `while` loop doesn't loop properly in an async way. It just loops infinitely – TKoL Feb 16 '18 at 09:15
  • @Aditya Oops, you're right, I misjudged that. However, you should simply use `emptyPromise = Promise.resolve()` for the initialisation, you are missing a `return` from the outermost `then` callback, and indeed that `while` loop approach would only work if you knew beforehand how many pages there were. Even then, in your particular implementation you never increment `currentPage` so the loop never terminates, and when you did you'd still have the closure-in-a-loop problem. – Bergi Feb 16 '18 at 15:43
  • @TKoL do you mean while loops infinitely in the code I wrote above? That's not the case – Aditya Feb 17 '18 at 17:10
  • @Bergi no that's not the case either. I just assumed that page 1 exists. There are two res in my code. The one in promise is resolver method and another one is result obtainer from first ApI call. Hence the approach is generic because current page and Total pages are keys in result obtained from callAPI. You are right that I could use Promise.resolve for init stage but I referred for more clarity if someone is not use to of promises. – Aditya Feb 17 '18 at 17:13
  • @Aditya I understood that you got the total number of pages from the first api call (which is fine when the API works like that), but my points still hold for the rest of the code. And yes, TKoL is right that your loop is infinite - the condition never changes during the body evaluation – Bergi Feb 17 '18 at 18:10
  • @Bergi oh yeah, I got the mistake. While loops that I mentioned goes infinite. To rectify it we need to make recursive call. Thank you though – Aditya Feb 18 '18 at 15:03