Delaying a Meteor.call on client side until a file upload has been completed

Dear all,
I am quite new to the community, so hello everyone. I need help or advise on how to achieve the following. I am creating a form where I am uploading a pdf document and a picture into two separate collections using ostrio:files (the code to upload is done client side as I followed their documentation). The resulting id of each is then to be added to an object which is then passed to a Meteor.call(). I am using check() in my Meteor method to ensure that data is proper. The problem is one of a race condition. The files get uploaded, but the Meteor.call() gets fired up before an id could be returned from the file upload, and since checking that id to be a String, the whole Meteor.call() fails. The files are however successfully saved in their respective collections, and the ids for these files are successfully added to my data object. Below is my code, and further below a screenshot of the browser console. My knowledge of promises and async/await functions is very limited. Any help, guidance in how to resolve this situation is highly appreciated.

Meteor method

Meteor.methods({
  // Method to add a doc
  'metCollBookShelf.addDoc': function(docData) {
    console.log(docData);

    if(Roles.userIsInRole(this.userId, ['superadmin', 'bookShelfAdmin'])) {
      
      // Check all inputs
      // Check directly all mandatory fields
      check(docData.title, String);
      check(docData.desc, String);
      check(docData.mainAuthor, String);
      check(docData.bookContentsId, String);
      // Conditionally check inputs if there is a value
      if(docData.aboutAuthor) { check(docData.aboutAuthor, String); }
      if(docData.coAuthors) { check(docData.coAuthors, Array); }
      if(docData.publisher) { check(docData.publisher, String); }
      if(docData.publicationDate) { check(docData.publicationDate, Date); }
      if(docData.language) { check(docData.language, String); }
      if(docData.ISBN) { check(docData.ISBN, String); }
      if(docData.bookCoverId) { check(docData.bookCoverLocId, String); }
      
      const docOwner = this.userId;

      // Insert Doc
      // On insertion, grab the docId for logging
      const docId = CollBookShelf.insert(docData);

      // Event Logging
      const logEventData = docData;
      logEventData.docId = docId;
      logEventData.docOwner = docOwner;
      Meteor.call('metCollAppLog.addDoc', 'x010101', 'addDoc', logEventData, function(err) {
        if (err) {
          throw new Meteor.Error('ErrorLog', TAPi18n.__('gen.msg.errorLogging'));
        }
      });

    } else {
      throw new Meteor.Error('Unauthorised', TAPi18n.__('gen.msg.unauthorisedAddDoc'));
    }

  },

Ostrio:files collection definition

export const MFCollBookShelfContents = new FilesCollection({
  collectionName: 'mfCollBooKShelfContents',
  allowClientCode: false,
  onBeforeUpload(file) {
    if(Roles.userIsInRole(this.userId, ['superadmin', 'bookShelfAdmin'])) {
      return true;
    }
    return 'You are not authorised to upload files';
  },
  onBeforeRemove(file) {
    if(Roles.userIsInRole(this.userId, ['superadmin', 'bookShelfAdmin'])) {
      return true;
    }
    return 'You are not authorised to upload files';
  }
});


// Deny interactions from client
if (Meteor.isServer) {
  MFCollBookShelfContents.denyClient();
}

Form submit event (BlazeJs)

// =================================================== //
  // Submit Document                                     //
  // =================================================== //
  'submit #blz-form-book-shelf-add'(evt) {
    evt.preventDefault();

    // Create docData object
    const docData = {};

    // Grab value and put in the object. Object property name should match db field names. Refer to associated method.
    docData.title = evt.target.bookTitle.value;
    docData.desc = evt.target.bookDesc.value;
    
    // Insert the cover first because the contents file may be bigger
    const fileObj_Cover = evt.target.blz_inp_book_cover;
    // console.log(fileObj_Cover);
    if (!fileObj_Cover.files[0]) {
      // docData.bookCoverLoc = '/images/default/noBookCover.jpg';
      docData.bookCoverId = '';
    } else if (fileObj_Cover.files && fileObj_Cover.files[0]) {

      const uploadCover = MFCollBookShelfCovers.insert({
        file: fileObj_Cover.files[0],
        streams: 'dynamic',
        chunkSize: 'dynamic'
      }, false);

      uploadCover.on('start', function () {

      });

      uploadCover.on('end', function (err, fileObj) {
        if (err) {
          throw new Meteor.Error('Upload error!', 'There was an error while uploading your file.');
        } else {
          docData.bookCoverId = fileObj._id;
          console.log(fileObj);
          console.log(docData);
        }
      });

      uploadCover.start();

    }
    
    // Upload book contents
    const fileObj_Contents = evt.target.blz_inp_book_contents;

    if (fileObj_Contents.files && fileObj_Contents.files[0]) {
      const uploadContents = MFCollBookShelfContents.insert({
        file: fileObj_Contents.files[0],
        streams: 'dynamic',
        chunkSize: 'dynamic'
      }, false);

      uploadContents.on('start', function () {

      });

      uploadContents.on('end', function (err, fileObj) {
        if (err) {
          throw new Meteor.Error('Upload error!', 'There was an error while uploading your file.');
        } else {
          docData.bookContentsId = fileObj._id;
          console.log(fileObj);
          console.log(docData);
        }
      });

      uploadContents.start();
    }
    
    // Insert the Doc
    Meteor.call('metCollBookShelf.addDoc', docData, function (err, res) {
      if (err) {
        toastr.error('There was an error saving your information.', 'Error.');
      } else if(res){
        toastr.success('Your publication has been stored on the bookshelf.', 'Well done!')
      }
    });

Console in chrome. You will see that the data object does not include the file ids, but gets added later after the file has been successfully uploaded. But then, it’s too late since Meteor.call has already been fired.

Not at computer right now --> but async might help https://blog.meteor.com/using-promises-and-async-await-in-meteor-8f6f4a04f998?gi=98874fcbb253

Hi pasharayan,
Wow. I never though I would receive a reply so fast. You guys are great. Thank you. I saw the article you sent already and even read it two three times. I must confess, I am clueless as to how to implement this in my given circumstance. I have watched also many youtube videos on async/await and I understood the basic concept while watching those setTimeout() examples. Beyond that, I remain clueless as to how to implement it. Again, I am not a dev by profession and has just recently started studying JS and Meteor. If you could help me out and explain these concepts and how it would apply that would be great. Thanks.

Something along the lines of

   var response = Async.runSync(function(done) {
        HTTP POST stuff here
    });

  response is now a variable you can use

In Meteor Files you have a handler for when the upload is complete, which I believe you use twice in your example.:

upload.on('end', function (error, fileObj) {})

Perhaps this is where you can/should call your method.

After doing some extensive learning, I manage to learn how to do promises. :slight_smile: For what it’s worth, here is the code I ended up with. Maybe it will be helpful for someone else.

// Example of collection definition for ostrio:files
// Example also shows using alanning:roles (for role-based access control) and tap:i18n (for internationalisation)

export const BookContents = new FilesCollection({
 collectionName: 'bookContents',
 storagePath: '/DATA/meteor/uploads',
 allowClientCode: true, //allow file removal from client
 onBeforeUpload(fileObj) {
   if (Roles.userIsInRole(this.userId, ['superadmin', 'bookAdmin'])) {
     return true;
   }
   return TAPi18n.__('gen.msg.UnauthorisedUpload');
 },
 protected(fileObj) {
   if (Roles.userIsInRole(this.userId, ['superadmin', 'bookAdmin', 'bookViewer'])) {
     return true;
   }
   return false;
 },
 onBeforeRemove(fileObj) {
   if (Roles.userIsInRole(this.userId, ['superadmin', 'bookAdmin'])) {
     return true;
   }
   return TAPi18n.__('gen.msg.UnauthorisedUpload');
 }
});


// Defining function with promise to upload contents
const uploadContents = function () {
 let promise = new Promise((resolve, reject) => {
   BookContents.insert({
     file: bookContents,
     onUploaded(err, fileObj) {
       if (err) {
         reject(TAPi18n.__('gen.msg.UploadErrorInfo'));
       } else {
         docData.bookContentId = fileObj._id;
         docData.bookContentLoc = '/cdn/storage/bookContents/' + fileObj._id + '/original/' + fileObj._id + fileObj.extensionWithDot;
         resolve(fileObj);
       }
     },
     streams: 'dynamic',
     chunkSize: 'dynamic'
   });
 });

 return promise;
};

// Defining function with promise to upload cover
const uploadCover = function () {
 let promise = new Promise((resolve, reject) => {
   if (!bookCover) {
     docData.bookCoverId = 'default';
     docData.bookCoverLoc = '/images/default/noBookCover.jpg';
     resolve();
   } else {
     BookCovers.insert({
       file: bookCover,
       onUploaded(err, fileObj) {
         if (err) {
           reject(TAPi18n.__('gen.msg.UploadErrorInfo'));
         } else {
           docData.bookCoverId = fileObj._id;
           docData.bookCoverLoc = '/cdn/storage/bookCovers/' + fileObj._id + '/original/' + fileObj._id + fileObj.extensionWithDot;
           resolve(fileObj);
         }
       },
       streams: 'dynamic',
       chunkSize: 'dynamic'
     });
   }
 });

 return promise;
}

// Chaining promises and executing Meteor call on success
uploadContents()
 .then(uploadCover)
 .then(() => {
   // Insert the Doc
   Meteor.call('bookAdd', docData, function (err, res) {
     if (!err) {
       toastr.success(TAPi18n.__('gen.msg.AddSuccess'), TAPi18n.__('gen.Success'));
     } else {
       toastr.error(TAPi18n.__('gen.msg.ErrorSave'), TAPi18n.__('gen.Error'));

     }
   });

 })
 .catch((message) => {
   toastr.error(message, TAPi18n.__('gen.Error'));
 });

Thank you all.