0

I am working on a project and on the newer side of Node. This is someone else's original implementation and I'm having an issue with promises and I know it most likely has to do with async/await or promise.all but I've spent alot time trying and still not getting the desired result.

I am going to the PlaceStore and returning a promise to get a list of people from a sql database. After I have the list of people. I map over the the list so I can have each person's id. Inside the map (which may be the problem because maps are not asynchronous) I am trying to find the most recent thing done by that person by calling the getMostRecentThingByPerson function that goes to the ThingStore to get the most recent thing.

When I log the resent in the result of the getMostRecentThingByPerson it does log the correct values but I don't know how to make the value available in the map function so I can update and return the object containing the 'Most Recent Thing' and export it to Csv.

I've read alot about how this is an important topic and thank you so much for your guidance with this issue.

exports.exportPeople = (req, res, next) => {
    const id = req.body.id;
    const place = req.place;

    PlaceStore.findPerson(place, async (err, people) => {
        if (err) {
            return new Error(err);
        }
        const peopleForCsv = people.map((person) => {
            const mostRecentThing = this.getMostRecentThingByPerson(person.id) // tried adding await here but does not work because of the map.

            return {
                'First Name': person.first_name,
                'Last Name': person.last_name,
                'Most Recent Thing': mostRecentThing
            };
        });
        const json2csvParser = new Parser({ delimiter: ',' });
        const csv = json2csvParser.parse(peopleForCsv);

        return res
            .status(200)
            .attachment('people_' + id + '.csv')
            .set('Content-Type', 'text/csv')
            .send(csv);
    });
};

exports.getMostRecentThingByPerson= (id) => {
    ThingStore.getThings({"person_id": id}, (err, things) => {
        if (err) {
            return new Error(err);
        }

        result = things.rows.length > 0 ? things.rows[0].capture_date : ''
        console.log(`Recent thing result for ${id}`, result) //example date
        return result
    })
    return result
}
DLM009
  • 1
  • 1
  • 1
    To use async/await properly, you must [promisify](https://stackoverflow.com/q/22519784/1048572) the `ThingStore.getThings` and `PlaceStore.findPerson` methods. – Bergi Jan 26 '23 at 18:35
  • Then, add the `await`, **and** use `Promise.all` as you guessed. – Bergi Jan 26 '23 at 18:36
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – gre_gor Jan 26 '23 at 19:49

1 Answers1

1

As mentioned in the comments below your question, you need to promisify your callback-based code. This is a fundamental pattern with which you need to get familiar.

To promisify your function getMostRecentThingByPerson, you need to create and return a promise which resolves on the result or rejects on the error (if any):

exports.getMostRecentThingByPerson = (id) => {
  return new Promise((resolve, reject) => {
    ThingStore.getThings({"person_id": id}, (err, things) => {
      if (err) {
        reject(err);
      } else {
        const result = things.rows.length > 0 ? things.rows[0].capture_date : ''
        console.log(`Recent thing result for ${id}`, result) // example date
        resolve(result)
      }
    })
  })
}

Now, getMostRecentThingByPerson returns a promise, so you need to await its resolution when calling it. If you do that inside a loop, you need to await the completion of each promise that will be created by each iteration of the loop:

exports.exportPeople = (req, res, next) => {
  const id = req.body.id;
  const place = req.place;

  PlaceStore.findPerson(place, async (err, people) => {
    if (err) {
       return new Error(err);
    }
    const peopleForCsv = await Promise.all(people.map(async (person) => {
      const mostRecentThing = await this.getMostRecentThingByPerson(person.id)

      return {
        'First Name': person.first_name,
        'Last Name': person.last_name,
        'Most Recent Thing': mostRecentThing
      }
    });

    const json2csvParser = new Parser({ delimiter: ',' });
    const csv = json2csvParser.parse(peopleForCsv);

    return res
      .status(200)
      .attachment('people_' + id + '.csv')
      .set('Content-Type', 'text/csv')
      .send(csv);
  });
};

Note that your current code for exportPeople does not return anything so it relies on a side-effect from calling PlaceStore.findPerson. I don't know if that works but I think a better practice would be to promisify PlaceStore.findPerson instead, and return its promised value (csv). Then you await it and return res.status(200)/* rest of the code */

Ben
  • 1,331
  • 2
  • 8
  • 15
  • I think the map inside promise.all is incorrect. The closure passed to map is synchronous. The proper form would produce an array of promises, not an array of objects that contain a promise. Less urgent, but `PlaceStore.findPerson` should also be promisified, so as not to mix the two styles. – danh Jan 26 '23 at 22:06
  • Are you sure you read my answer? The function passed to Array#map is async, so it returns a Promise, so `people.map` will return an array of promises. The return is an object containing an awaited promise, so not a promise. And I already mentioned in my answer that `PlaceStore.findPerson` should be promisified as well... – Ben Jan 27 '23 at 12:48
  • Sorry @Ben, I didn't read carefully enough. – danh Jan 27 '23 at 18:44