0

I have an api route that needs to take data from two sources, merge the data together into one object and then return. The issue I am having is I'm basically stuck in async/await hell and when pushing to a second array within the .then() block, the second array named clone returns []. How can I make an api request, merge the data and return to the requester as needed?

Fetch code:

export default async function getProduct(product_id) {
  const product = await fetch(
    `${process.env.PRIVATE_APP_URL}/products/${product_id}.json`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    }
  ).then((result) => {
    return result.json();
  });
    return product.product;
}

API Handler:

const recharge_subscription_res = await rechargeAPI(
  "GET",
  `https://api.rechargeapps.com/subscriptions?customer_id=${recharge_customer.id}`
);

const closest_array = recharge_subscription_res.subscriptions.filter(
  (e) => e.next_charge_scheduled_at == closest_date
);

let clone = [];

closest_array.forEach((element) => {
  getProduct(element.shopify_product_id).then((product) => {
    element.shopify_product_handle = product.handle;
    element.shopify_product_image_url = product.image.src;
    clone.push(element);
  });
});

console.log(clone);

clone should log as an array of objects like closest_array, but instead logs as just an empty array. This isn't exactly like the other seemingly duplicate questions because typically their feature doesn't require sending the promise's data back to an external source. Most questions are related to the front end of things. My situation is with an Express.js API. Any help would be appreciated.

G.Rose
  • 644
  • 7
  • 29
  • You are logging it before anything is even added to it! getProduct is asynchronous. – CherryDT Apr 04 '22 at 19:59
  • @CherryDT maybe I should've mentioned this in the post, but the returned data from the call is also an empty array – G.Rose Apr 04 '22 at 20:29

2 Answers2

1

Your code has a flaw (in the section shown below). You have a dangling promise that you forgot to await or return. When you log clone, none of the async getProduct operations have completed yet, and none of the elements have been pushed.

let clone = [];

closest_array.forEach((element) => {
  getProduct(element.shopify_product_id).then((product) => {
    element.shopify_product_handle = product.handle;
    element.shopify_product_image_url = product.image.src;
    clone.push(element);
  }); // FLAW:  dangling .then
});
console.log(clone); // FLAW: clone is not ready yet.

I would set it up more like this:

let clone = await Promise.all(closest_array.map((element) =>
  getProduct(element.shopify_product_id).then((product) => {
    element.shopify_product_handle = product.handle;
    element.shopify_product_image_url = product.image.src;
    return element;
  })
));
console.log(clone);

It's a little sketchy to modify element the way you are (I wouldn't), but this way the getProduct calls are all in flight together for maximum efficiency. Promise.all handles awaiting all the promises and putting each's result into an array of results, which you can then await as a single promise since the calling function is async.

Wyck
  • 10,311
  • 6
  • 39
  • 60
  • Thank you for your response, just curious about how you would modify `element` in this scenario. The more feedback I can get the better – G.Rose Apr 04 '22 at 22:44
  • @G.Rose I think I was surprised that you create `clone` which is a copy of the `closest_array`, but with all the same elements (literally the same objects) - which makes me wonder why you have a copy of the array at all. On the other hand if each element of clone were a *copy* of each element in `closest_array` then it seems reasonable to make a copy. In your case I think you are just adding a few properties to each element so I'm not sure you need a new `clone` new array at all. It's hard to know your full intentions just from what you provided. – Wyck Apr 05 '22 at 16:43
  • you're probably right I don't think I need the clone. I had it set that way initially because I was thinking I needed to push to the mutated object in the foreach to a clone array to escape the promises, but of course that wasn't the case – G.Rose Apr 05 '22 at 16:45
1

The original promise spec used .then(), and the new syntax hides then's with await. Style-wise, it makes sense to choose just one style and go with it.

In either style, there's a little challenge having to do with creating many promises in a loop. The js iteration functions (like map and forEach) take synchronous functions. The most common design is to create a collection of promises in a synchronous loop, then run them concurrently with Promise.all(). Taking both ideas into account...

You could (but don't have to) rewrite your network request like this...

// since we decorated "async" let's use await...
export default async function getProduct(product_id) {
  const url = `${process.env.PRIVATE_APP_URL}/products/${product_id}.json`;
  const options = { method: "GET", headers: { "Content-Type": "application/json" }};
  const result = await fetch(url, options);
  const product = await result.json();
  return product.product;
}

await is not permitted at the top-level; it may be used only within an async function. Here, I'll make up a name and guess about the parameter

async function rechargeAndLookupProduct(recharge_customer) {
  const base = 'https://api.rechargeapps.com/subscriptions';
  const query = `customer_id=${recharge_customer.id}`;
  const recharge_subscription_res = await rechargeAPI("GET",`${base}?${query}`);

  const closest_array = recharge_subscription_res.subscriptions.filter(e =>
    e.next_charge_scheduled_at == closest_date
  );

  // here's the important part: collect promises synchronously
  // execute them together with Promise.all()
  const promises = closest_array.map(element => {
    return getProduct(element.shopify_product_id)
  });
  const allProducts = await Promise.all(promises);
  // allProducts will be an array of objects that the promises resolved to
  const clones = allProducts.map((product, i) => {
    // use Object.assign so we'll really have a "clone"
    let closest = Object.assign({}, closest_array[i]);
    closest.shopify_product_handle = product.handle;
    closest.shopify_product_image_url = product.image.src;
    return closest;
  });
  // if I didn't make any typos (which I probably did), then
  // clones ought to contain the result you expect
  console.log(clones);
}
danh
  • 62,181
  • 10
  • 95
  • 136