Search code examples
node.jsasync-awaitwritable

Async/Await In NodeJS


This works in NodeJS but I have to use recordObject within the codeblock where it was created.
It is not visible outside of the code block even if recordObject is declared at the beginning of the function because the function proceeds before the recordObject has been retrieved. I want to do this using async/await but I don't know where to grab hold of recordObject after it has been returned from helpers.getMostRecent.

Here is the entire function.
Can someone please show me how to do this using Async/Await?
Thanks, John

// Define a function which builds a webpage and all supporting client/server code for 
// adding records to a json table in the database.
meta.build.AddWebpage = function(tableId)
{
  let dataObject = {};
  dataObject.uniqueField01Value = "";   
  dataObject.uniqueField01Name = "table.tableName";           
  dataObject.path = '/dbMetadata/metadata.json';
  dataObject.queryString = 'WHERE:;tableId:;MatchesExactly:;' + tableId + ':;';  

  // Collect information about the webpage from the metadata.
  // 1. Look in metadata.json - Read the object for the given tableId.
  helpers.getMostRecent(dataObject, function(errorFromGetMostRecent, payload)
  {
    if(!errorFromGetMostRecent) // Got the most recent record from getMostRecent
    {
      // Used to decode the payload buffer into readable text.
      let decoder = new StringDecoder('utf8');    

      // This instance of the Writable object gives us a place for a callback to run when the payload is received.
      const writable = new Writable();

      // Called by pipeline below. Does something useful with the payload
      writable.write = function(payload)     
      {
        let stringContainer = '';                 
        stringContainer = stringContainer + decoder.write(payload);
        let recordObject = JSON.parse(stringContainer);

        // recordObject is all the metadata for the table.
        // I can use recordObject to build the webpage and all supporting code for adding records to a table
        ///////////////My problem is I have to do everything else in this code block////////////////////
        ?????????????????????????????How can I change this to Async/Await???????????????????????????????

      }; // End of: writable.write = function(payload){...}

      // Passes the payload stream to the writable object which calls writable.write 
      // which does something useful with the payload.
      pipeline
      (
        payload,
        writable,
        function(error){if(error){console.log('There was an error.');}}
      );

    } // End of: if(!error) Got the most recent record from gitHashedPass
    else // There was indeed an error returned by getMostRecent when attempting to get the most current record.
    {
      helpers.log // Log the error.
      (
        7,
        'bxpa2p2t7ps3wrd1dqu7' + '\n' + 
        'The following was the error message from getMostRecent:' + '\n' +                                             
        errorFromGetMostRecent + '\n'                                                 
      ); // End of: helpers.log // Log the error.
    } // End of: Else // There was indeed an error returned by getHashedPass when attempting to get the most current record.



  }); //End of: helpers.getMostRecent(dataObject, function(errorFromGetMostRecent, payload)

  // Assemble the webpage string from the metadata in recordObject

}// End of: meta.build.AddWebpage = function(tableId){...}

Here is the function getMostRecent which is called by the function above to get the json record about the table we wish to add to the accounting system.

// Define a function to retrieve the most current record in a table for a given primary key.
// Serves as table validation for put handlers (editing existing records)
// Checks that a record with the supplied primary key exists to modify.
// Also checks that a candidate values supplied for a unique fields are not already in the table. Enforces uniqueness.
// Streams the most current record back to the calling function.
helpers.getMostRecent = function(data, callback)
{
  // No value set but declared here because we need access to these thoughout the entire function.
  let queryArray, queryString;

  // typical example of a queryString: 'WHERE:;userId:;MatchesExactly:;' + userId + ':;'
  queryString = data.queryString

  // Make an array out of the queryString where each phrase of the query is an element.
  queryArray = queryString.split(":;");       

  // Create an empty map data structure which will be used to merge records with the same primary key. 
  // Hard if not impossible do that with objects.
  let tableMap = new Map();

  // Create a variable to track whether or not candidate values for any unique fields have already been used.
  let uniqueValuesAlreadyUsed = false;

  // Create a variable to track the primary key of a record that we may encounter which 
  // is holding a candidate unique value (perhaps and email address or phone number).
  // If we encounter this primary key again as we proceed through the records then we will 
  // check to see if the candidate unique value has been changed or if the record has been deleted. 
  // If so we will set this variable to false signifying that the candidate unique value is available again.
  let primaryKeyOfRecordHoldingCandidateUniqueValue = false

  // This function sets up a stream where each chunk of data is a complete line in the table file.
  let readInterface = readline.createInterface
  (
    { // specify the file to be read.
      input: fs.createReadStream(helpers.baseDir + data.path)
    }
  );

  // Look at each record in the file.
  readInterface.on('line', function(line) 
  {
    // Convert the JSON string (a single line from the table file) into lineValueObject.
    let lineValueObject = JSON.parse(line);    

    // Declare a variable to serve as a key in the map to manage the lineValueObject.
    let primaryKey = lineValueObject[queryArray[1]];  

    let shouldDeleteThisRecord = false; 

    if(lineValueObject.deleted === true) // if the record in the table file had the delete field set to true:
    {
      // If this record was holding our candidate unique field value:
      if(lineValueObject[queryArray[1]] === primaryKeyOfRecordHoldingCandidateUniqueValue) 
      {
        // The record holding our candidate unique field value has been deleted so...

        // The candidate unique field value is available.
        uniqueValuesAlreadyUsed = false;

        // There is no more reason to track this record.
        primaryKeyOfRecordHoldingCandidateUniqueValue = false;          
      }

      // This is not the record we are trying to modify.
      // Remove this record from the map. 
      shouldDeleteThisRecord = true;      
    }
    else // The record was not deleted
    { 
      // If the current record does not have a primary key matching the record we wish to change:
      if(lineValueObject[queryArray[1]] != queryArray[3])
      {
        // Check if this record has the same unique field value we wish to write. 
        // In other words: Has the unique field value already been used?
        if(lineValueObject[data.uniqueField01Name] === data.uniqueField01Value)
        {
          // Currently this unique field value is taken. 
          // As we proceed, we may encounter a record with this same primary key where the unique field value has been changed.
          // Or, with this same primary key that has been deleted. 
          // Either case will make the unique field value available again as we proceed through the records
          // So we need to compare the primary key of this record to other records that we encounter.

          // Flag that the unique field value is already being used.
          uniqueValuesAlreadyUsed = true;

          // Take note of the primary key so that we can check as we proceed if this record gets 
          // deleted or if the unique field value gets changed.
          primaryKeyOfRecordHoldingCandidateUniqueValue = lineValueObject[queryArray[3]];
        } // End of: Check if this record has the same unique field value we wish to write. Is the unique field value already taken?

        // Well then - Not deleted, not the same key as the record we want to change, not the candidate unique field value so...
        // Check if this record was previously holding the candidate unique field value but is now changed.
        else if
        (
          primaryKeyOfRecordHoldingCandidateUniqueValue === lineValueObject[queryArray[1]]
          &&
          lineValueObject[data.uniqueField01Name] != data.uniqueField01Value 
        )
        {
          // This record was tying up the candidate unique field value but is no longer holding it.
          // The candidate unique field value is available again.
          uniqueValuesAlreadyUsed = false;

          // There is no more reason to track this record.
          primaryKeyOfRecordHoldingCandidateUniqueValue = false;           
        }

        // This is not the record we are trying to modify.
        // Remove this record from the map.
        shouldDeleteThisRecord = true;
      } // End of: If the current record does not have a primary key matching the record we wish to change:

    } //End of: else - The record was not deleted

    // If the record was not marked for deletion and has the primary key of the record we wish to change:
    // This is the record we are going to send back to the calling function.
    if(shouldDeleteThisRecord == false)
    {          
      // Update this record in the map.
      tableMap.set(primaryKey, lineValueObject);
    }
    else // shouldDeleteThisRecord is true. This is not the record we are trying to modify. Don't send it back.
    {
      tableMap.delete(primaryKey);
    }

  }); // End of: readInterface.on('line', function(line){...}
  // End of: Look at each record...


  // This listener fires after we have looked through all the records in the table file.
  // The callback function defined here will stream one record back to the clients browser.
  readInterface.on('close', function() 
  {          
    // This readable stream will be used to write the result of the merge to a new file.
    const sourceStream = new Readable(); 

    // Check that the specified record was found.
    if(tableMap.size === 0)
    {
      helpers.log
      (
        4,
        'xwmv16fc90bzrbnvhbg5' + '\n' +
        'No record found for primary key' + '\n'
      );
            //params:  error 
      return callback("No record found for primary key");
    }

    if(uniqueValuesAlreadyUsed === true)
    {
      helpers.log
      (
        4,
        'xwmv16fc90bzrbnvhbg5' + '\n' +
        'This ' +  data.uniqueField01Name + ' value already exists' + '\n'
      );
            //params:  error 
      return callback('This ' +  data.uniqueField01Name + ' value already exists');
    }

    for (const [key, valueObject] of tableMap)
    {
      // Convert the data object to a string.
      let stringData = JSON.stringify(valueObject);     

      // Load the readable stream with data.
      sourceStream.push(stringData + '\n');                  
    }                

    // Tell the stream no more data is coming.
    sourceStream.push(null);     

    //params:  error, json record
    callback(false, sourceStream);             

  }); // End of: readInterface.on('close', function(){...}   

}; // End of: helpers.getMostRecent = function(data, callback){...}
// End of: // Define a function to retrieve the most current record in a table for a given primary key.


Solution

  • I would suggest reading https://alligator.io/js/async-functions/ or similar tutorial to get a good understand of async/await. With this you can potentially rewrite your functions and make your flow easier to work with.

    A simple example of rewriting a callback function to a function using async/await could look like this:

    Without async/await:

    function consumerFunction() {
      myFunction(1, function(error, value) {
        console.log(error, value); // outputs: null, 2
      });
    }
    
    function myFunction(value, callback) {
      setTimeout(() => { // mimick async behaviour
        callback(null, value + 1);
      }, 1000);
    }
    

    With async/await:

    async function consumerFunction() {
      try {
        const value = myFunction(1);
        console.log(value); // outputs 2
      } catch (error) {
        // Code to handle potential errors
      }
    }
    
    function myFunction(value) {
      return new Promise(function (resolve, reject) {
        setTimeout(() => { // mimick async behavoir
          resolve(value + 1); // success case
          // reject('Something bad happened'); // error case
        }, 1000);
      })
    }