How to write a Cursor Observe Pubblication

Hello, i am updating multiple documents inside my collection and i wanted a way to send back to the client the updated documents.

'rebates.update': function rebatesUpdate(doc) {
    check(doc, {
      quantity: Number,
      amount: Number,
      brand: String,
    });

    const { quantity, amount, brand } = doc;
    if (Roles.userIsInRole(Meteor.userId(), 'admin')) {
      try {
        const results = Rebates.find({ amount, brand, printed: false }, { limit: quantity });

        results.forEach((rebate) => {
          try {
            Rebates.update(rebate._id, { $set: { printed: true } });
          } catch (error) {
            throw new Meteor.Error('500', error);
          }
        });
      } catch (exception) {
        throw new Meteor.Error('500', exception);
      }
    }
  },

Inside the component that calls the update method i am tryng to write a pubblication that uses cursor.observe but i am stuck

Meteor.publish('rebatesFalseCount', function rebates() {
  if (Roles.userIsInRole(this.userId, 'admin')) {
    const subscription = this;

    const isPrintedFalseHandle = Rebates.find({ printed: false }).observe({
      changed(newObj, oldObj) {
        console.log('hello from me', oldObj); // i get always undefined
       
// i dont know how to return the objects back to the client
      },
    });

    subscription.ready();

    subscription.onStop(() => {
      isPrintedFalseHandle.stop();
    });
  }
});
1 Like

You can use the changed method on the current context. In your case.

subscription.changed(Rebates._name, oldObj._id, newObj);

You could be a bit more efficient if you diff the old and new, but this will work fine this way as well.

can you show me the complete code for this subscription, where should i put subscription.changed, i just replace it with self.changed?

the last piece i am missing is on the client how to get this data.

export default withTracker(() => {
  const subscription = Meteor.subscribe('rebatesFalseCount');

  return {
    loading: !subscription.ready(),

    documentsChanged: // ?????? how should i do it in here
  };
})(DownloaderGenerator);
Meteor.publish('rebatesFalseCount', function rebates() {
  if (Roles.userIsInRole(this.userId, 'admin')) {
    const subscription = this;

    const isPrintedFalseHandle = Rebates.find({ printed: false }).observe({
      changed(newObj, oldObj) {
        subscription.changed(Rebates._name, oldObj._id, newObj);
      },
    });

    subscription.ready();

    subscription.onStop(() => {
      isPrintedFalseHandle.stop();
    });
  }
});
export default withTracker(() => {
  const subscription = Meteor.subscribe('rebatesFalseCount');

  return {
    loading: !subscription.ready(),
    documentsChanged: Rebates.find({ printed: false }),
  };
})(DownloaderGenerator);

it doesnt work, documentsChanges returns all the documents with printed false, and wenever i update the printed status the sub it does not trigger,

i have inserted a breakpoint into the pub but it never gets called.

I guess I’ve misunderstood what you are trying to accomplish then.

after i update the documents, i want to return to the client only
the updated documents

I think what you want to do is what publications and subscriptions do by default - meaning, all you need to do is serve up the whole vanilla cursor, and then subscribe. When something changes, only that document is sent (or those documents)

In this case, after you { $set: { printed: true } }, that document will be “removed” from the cursor .find({ printed: false }) because it no longer satisfies the selector. So you’ll have to also use the removed handler (ref).

changed handler would be useful for changes in fields other than printed, because they would still match the original selector.

If you want to continue to publish the doc with the new value of printed then you should change the selector accordingly

I’m personally not aware of any way to modify a set of documents and return only the changed documents from a publication… You could approach this in several other ways… You could try returning the documents directly from the method to the client. You could also generate something random but unique that you can set as a field on the documents to differentiate them and then return that identifier to the client from the method and use it to match against.

Also as a side note it would be much more efficient if you updated the documents in a single update instead of one for each document.

'rebates.update': function rebatesUpdate(doc) {
    check(doc, {
      quantity: Number,
      amount: Number,
      brand: String,
    });

    const { quantity, amount, brand } = doc;
    if (Roles.userIsInRole(this.userId, 'admin')) {
      try {
        const results = Rebates.find({ amount, brand, printed: false }, { limit: quantity });
        const rebateIds = results.map(rebate => rebate._id);
        Rebates.update({_id: { $in: rebateIds } }, { $set: { printed: true } });
      } catch (exception) {
        throw new Meteor.Error('500', exception);
      }
    }
  },

how would you do that with the code? i still dont understand how to get the messages used by this.added or this.changed. In @copleykj example above we say subscription.changed(Rebates._name, oldObj._id, newObj); and then we say subscription.ready(); , but on the client how would i print that message?

I don’t think you are trying to do anything in your custom publication that it doesn’t already do by default if you just return a cursor.

Mini-Mongo is just a document cache and if you publish 10 documents from one publication and 20 from another, then you’ll have 30 documents on the client and no way to tell them apart, and that’s what you need is a way to tell them apart.

In your method, you could generate an id or a timestamp that gets added to the document and then return that to the client so you can tell which documents were updated.

'rebates.update': function rebatesUpdate(doc) {
    check(doc, {
      quantity: Number,
      amount: Number,
      brand: String,
    });

    const { quantity, amount, brand } = doc;
    if (Roles.userIsInRole(this.userId, 'admin')) {
      const prinitedAt = new Date(); // <--- this is your way to identify changed documents

      try {
        const results = Rebates.find({ amount, brand, printed: false }, { limit: quantity });
        const rebateIds = results.map(rebate => rebate._id);

        Rebates.update({_id: { $in: rebateIds } }, { $set: { printedAt } });

        return printedAt;
      } catch (exception) {
        throw new Meteor.Error('500', exception);
      }
    }
  },
const printedAt = new ReactiveVar();

Meteor.call('rebates.update', (error, result) => {
  if (!error) {
    printedAt.set(result);
  }
});


export default withTracker(() => {
  const subscription = Meteor.subscribe('rebatesFalseCount');

  return {
    loading: !subscription.ready(),
    documentsChanged: Rebates.find({printedAt: printedAt.get()}),
  };
})(DownloaderGenerator);
2 Likes

Why not use publish composite https://github.com/englue/meteor-publish-composite

I’m pretty certain this isn’t an issue where publish-composite is an appropriate solution.

thank you, everyting worked perfectly with you’re aproach.

1 Like