0

Simple code path, that is better explained with bullet points

  1. API call
  2. Call to DB
  3. Result is a list of objects
  4. Inside the then block make a DB call for each object to hydrate children
  5. Inside another then block res.send(hydrated object)

Problem

Step 5 happens before step 4 completes (Code below)

//api endpoint
router.post('/get-data', getObjects);

export const getObjects: express.RequestHandler = (req, res) => {
    queryContainer(containerId, querySpec)
    .then((result) => {
        return getChildren(result, req.body.criteria);
    })
    .then((result) => {
        res.send(result);
    });
}

export async function queryContainer(containerId, querySpec) {
    const { result: results } = await client.database(databaseId).container(containerId).items.query(querySpec, {enableCrossPartitionQuery: true}).toArray()
    .catch( (error) => {
        console.log("Error! ", error);
    });
    return results;
}

function getChildren(result: any, criteria: any) {
    if(criteria) {
        result.children = [];
        var actions = result
                        .map(result => result.element)
                        .map(dbCallToGetChildren);
        var results = Promise.all(actions);
        results.then(children => {
            result.children.push(...children)
            return result;
        });
    } 

    return result;
}

export const dbCallToGetChildren = (async function (username) {
    const querySpec = {
        query: "SELECT * FROM root r WHERE r.userName=@userName",
        parameters: [
            {name: "@userName", value: username}
        ]
    };
    queryContainer(containerId, querySpec)
    .then((results) => { 
        return results;
    })
    .catch((error) => {
        console.log("Error " + error);
        return Promise.resolve;
    });
});
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
AnxiousdeV
  • 373
  • 1
  • 3
  • 20

3 Answers3

0

Step 5 happens before step 4 (getChildren function) completes because getChildren isn't returning a Promise. Changing it to the following might fix the problem:

function getChildren(result: any, criteria: any) {
  return new Promise(resolve => {
    if(criteria) {
        result.children = [];
        var actions = result
                        .map(result => result.element)
                        .map(dbCallToGetChildren);
        var results = Promise.all(actions);
        results.then(children => {
            result.children.push(...children)
            resolve(result);
        });
    } else {
      resolve(result);
    }
  });
}

Inside results.then(children => { ... } there is now resolve(result); to ensure the return getChildren(result, req.body.criteria); statement within the queryContainer call doesn't complete until the Promise resolves.

Pat Needham
  • 5,698
  • 7
  • 43
  • 63
0

In order for step 4 to fully complete before step 5 is executed, we need to promisify every code-path that goes through getChildren.

That means we should change this:

function getChildren(result: any, criteria: any) {
    if(criteria) {
        result.children = [];
        var actions = result
                        .map(result => result.element)
                        .map(dbCallToGetChildren);
        var results = Promise.all(actions);
        results.then(children => {
            result.children.push(...children)
            return result;
        });
    } 
    return result;
}

Into the following (pay attention to the code comments):

function getChildren(result: any, criteria: any) {
    if(criteria) {
        result.children = [];
        var actions = result
                        .map(result => result.element)
                        .map(dbCallToGetChildren);
        var results = Promise.all(actions);
        return results.then(children => { // returns a promise 
            result.children.push(...children)
            return result;
        });
    } 
    return Promise.resolve(result); // returns a promise
}

It's important to return otherwise the code in this line is being executed in async manner (which doesn't guarantee that step 4 will complete before step 5 begins).

Nir Alfasi
  • 53,191
  • 11
  • 86
  • 129
0

I have a few comments about your code:

  1. you are doing mutation which is a bad practice, getChildren function accepts result type any and then it makes some changes to it like

result.children = []

  1. Avoid using any and try to define a type

  2. in your code because you are doing mutation so you do not even need to return back the result because the original object is already changed but as I mentioned earlier you should avoid mutation.

  3. The main problem that step 5 executes before step 4 is that getChildren won't return back promise, I made some changes in your code to accommodate promise.

      function getChildren(result: any, criteria: any): Promise<any> {
      return new Promise((resolve, reject) => {
        if (criteria) {
          result.children = []
          const actions = result
            .map(item => item.element)
            .map(dbCallToGetChildren)
          Promise.all(actions).then(children => { // returns a promise
            result.children.push(...children)
            return resolve("successfully is processed!")
          })
        }
        reject("invalid criteria!")
      })
    }
Mehran
  • 100
  • 1
  • 6
  • Adding the promise has resolved the issue. Excellent solution, Mehran. – AnxiousdeV Apr 15 '19 at 07:30
  • In the famous words of @Bergi (who must have said it 1000 times), avoid [the explicit promise construction antipattern](https://stackoverflow.com/q/23803743/3478010). – Roamer-1888 Apr 15 '19 at 11:05
  • @Roamer-1888 I do agree with you, if I wanted to write the code, I would return back the whole promise instead of creating a new promise, but I did not have the full picture of the code and I just tried to address the issue in the very closed context. – Mehran Apr 16 '19 at 01:19