Git Product home page Git Product logo

cottz-publish-relations's Introduction

Cottz publish-relations

Edit your documents before sending without too much stress. provides a number of methods to easily manipulate data using internally observe and observeChanges in the server

Installation

$ meteor add cottz:publish-relations

Quick Start

Assuming we have the following collections

// Authors
{
  _id: 'someAuthorId',
  name: 'Luis',
  profile: 'someProfileId',
  bio: 'I am a very good and happy author',
  interests: ['writing', 'reading', 'others']
}

// Books
{
  _id: 'someBookId',
  authorId: 'someAuthorId',
  name: 'meteor for dummies'
}

// Comments
{
  _id: 'someCommentId',
  bookId: 'someBookId',
  text: 'This book is better than meteor for pros :O'
}

I want publish the autor with his books and comments of the books and I want to show only some interests of the author

import PublishRelations from 'meteor/cottz:publish-relations';

PublishRelations('author', function (authorId) {
  this.cursor(Authors.find(authorId), function (id, doc) {
    this.cursor(Books.find({authorId: id}), function (id, doc) {
      this.cursor(Comments.find({bookId: id}));
    });

    doc.interests = this.paginate({interests: doc.interests}, 5);
  });

  return this.ready();
});
// Client
// skip 5 interest and show the next 5
PublishRelations.changePag({_id: 'authorId', field: 'interests', skip: 5});

Note: The above code is very nice and works correctly, but I recommend that you read the Performance Notes

Main API

to use the following methods you should use PublishRelations instead of Meteor.publish

this.cursor (cursor, collection, callbacks(id, doc, changed))

publishes a cursor, collection is not required

  • collection is the collection where the cursor will be sent. if not sent, is the default cursor collection name
  • callbacks is an object with 3 functions (added, changed, removed) or a function that is called when it is added and changed and receive in third parameter a Boolean value that indicates if is changed
  • If you send callbacks you can use all the methods again and you can edit the document directly (doc.property = 'some') or send it in the return.

Note: when a document changes (update) doc contains only the changes, not the whole document.

this.join (Collection, options, name)

It allows you to collect a lot of _ids and then make a single query, only Collection is required.

  • Collection is the Mongo Collection to be used
  • options the options parameter in a Collection.find
  • name the name of a different collection to receive documents there

After creating an instance of this.join you can do the following

const comments = this.join(Comments, {});
// default query is {_id: _id} or {_id: {$in: _ids}}
// if you need to use another field use selector
comments.selector = function (_ids) {
  // _ids is {$in: _ids} or a single _id
  return {bookId: _ids};
};
// Adds a new id to the query
comments.push(id);
comments.push(id2, id3, id4);
// Sends the query to the client, after sending the query each new push()
// send a new query, you do not have to worry about reactivity or
// performance with this method
comments.send();

Why use this and not this.cursor? because they are just 2 queries

const comments = this.join(Comments, {});
comments.selector = _ids => {bookId: _ids};

this.cursor(Books.find(), function (id, doc) {
  comments.push(id);
});

comments.send();

this.observe / this.observeChanges (cursor, callbacks)

observe or observe changes in a cursor without sending anything to the client, callbacks are the same as those used by meteor

Nonreactive API

The following methods work much like their peers but they are not reactive

this.cursorNonreactive (cursor, collection, callback)

It has 2 differences with this.cursor

  • callback is only a function that executes when a document is added
  • you can only use non-reactive methods within the callback

this.joinNonreactive (Collection, options, name)

Is exactly the same as this.join but non reactive

Crossbar API

The following methods use Meteor Crossbar which allows the client to communicate with a publication without re-run the publication

this.paginate (field, limit, infinite)

page within an array without re run the publication or callback

  • returns the paginated array, be sure to change it in the document
  • field is an object where the key is the field in the document and the value an array
  • limit the total number of values in the array to show
  • infinite if true the above values are not removed when the paging is increased
  • PublishRelations.changePag({_id, field, skip}) change the pagination of the document with that id and field. skip is the number of values to skip.

this.listen (data, callback, run)

It allows you to execute a part of the publication when the client asks for it. It is easier to explain with an example

import PublishRelations from 'meteor/cottz:publish-relations';

PublishRelations('books', function (data) {
  const pattern = {
    authorId: String,
    skip: Match.Integer
  };
  check(data, pattern);

  if (!this.userId || !Meteor.users.findOne({_id: this.userId}))
    return this.ready();
  // Maybe you have roles or another validations here

  // If inside this.listen you'll use this, make sure to use an arrow function
  this.listen(data, (runBeforeReady) => {
    if (!runBeforeReady)
      check(data, pattern);

    this.cursor(Books.find({authorId: data.authorId}, {
      limit: 10,
      skip: data.skip
    }));
  });

  return this.ready();
});

// -- client --
Meteor.subscribe('books', {authorId: 'authorId', skip: 0});
// skip 10 books and show the next 10
// the second param must be an object
PublishRelations.fire('books', {authorId: 'authorId', skip: 10});

each time that you use PublishRelations.fire the listen callback is rerun, the param data that you sent in listen extends with the data sent in the fire event

  • run is a boolean value (default true). if true callback is executed immediately within the publication before the first fire
  • callback only receives the boolean parameter runBeforeReady that is only true when run is true and the callback runs for first time
  • you can have only one listen by publication

Performance Notes

  • all methods returns an object with the stop() method except for paginate
  • all cursors are stopped when the publication stop
  • when the parent cursor is stopped or a document with cursors is removed all related cursors are stopped
  • all cursors use basic observeChanges as meteor does by default, performance does not come down
  • if when the callback is re-executes not called again some method (within an If for example), the method continues to run normally, if you re-call method (because the selector is now different) the previous method is replaced with the new
// For example we have a collection users and each user has a roomId
// we want to publish the users and their rooms
this.cursor(Meteor.users.find(), function (id, doc) {
  // this function is executed on added/changed
  this.cursor(Rooms.find({_id: doc.roomId}));
});
// the previous cursor is good but has a bug, when an user is changed we can't make sure
// that the roomId is changed and 'doc' only comes with the changes, so roomId is undefined
// and our Rooms cursor no longer work anymore

// to fix the above problem we need to check the roomId
this.cursor(Meteor.users.find(), function (id, doc) {
  if (doc.roomId)
    this.cursor(Rooms.find({_id: doc.roomId}));
});
// or we can use an object with 'added' instead of a function
// this way is better than the above if we are sure that roomId is not going to change
this.cursor(Meteor.users.find(), {
  added: function (id, doc) {
    this.cursor(Rooms.find({_id: doc.roomId}));
  }
});
  • As I said in Quick Start you can do this
this.cursor(Authors.find(authorId), function (id, doc) {
  this.cursor(Books.find({authorId: id}), function (id, doc) {
    this.cursor(Comments.find({bookId: id}));
  });
});

but you will find that the publication is becoming increasingly slow, suppose you have 10 books for a given author and every book has 100 reviews, with this method would make the following queries: 1 author + 1 books + 10 comments = 12 queries, for each book found a query is made to find comments which creates a performance issue and publication could take seconds

The solution is to use this.join to join all the comments and send them in a single query, passing from 12 queries to 3 queries for mongo

const comments = this.join(Comments);
comments.selector = _ids => {bookId: _ids};

this.cursor(Authors.find(authorId), function (id, doc) {
  // We not have to worry about the books cursor because we only have one author
  this.cursor(Books.find({authorId: id}), function (id, doc) {
    comments.push(id);
  });
});

comments.send();
  • publications are completed as usual
// you can do this to finish writing your publication
this.ready();
return this.ready();
return [];
return [cursor1, cursor2, cursor3];

cottz-publish-relations's People

Contributors

lfades avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

cottz-publish-relations's Issues

Is 'this.ready()' obligatory?

The publication seem to work without this.ready(), but in documentation it is provided as example. Is this obligatory?

seems broken

Even the simplest publication throws

Publish function can only return a Cursor or an array of Cursors.

Help Plz: Filter/remove some documents from a this.cursor() - call?

Hello,

i'm rewriting parts of my app using cottz-publish-relations and it's going great so far! :D

But now I've come across a situation where I'm stumped and don't know how to handle it.

The task is the following:

I've got an Activity Stream (think Facebook wall) and objects which are published (let's say "stories").

But some of those stories should be private, or maybe only visible for certain people / groups. To keep this example simpler, let's say they're private and should only be visible for the user themselves, but not for others.

For the users themselves this is easy:

this.cursor(Activities.find({}, {sortBy: {timestamp: -1}})), function(activityId, activityDoc) {
  if (activityDoc.storyId) {
    this.cursor(Stories.find({activityDoc.storyId}));
  }
});

Or something similar to this, right? (Pseudocode, untested).

But what if I want to decide whether to publish an Activity only if the Story has some kind of flag set (let's say "public")?

How can I remove the activity from the Activities.find() - result while keeping everything reactive?

If I'd first query all activity IDs and then make a {_id: {$in: [storyIds]}} - query on the stories, new activities wouldn't be published after that, right?

And the other way round, if I'd first get all the stories to filter by those, I'd potentially have a million stories in my cursor and only 5 related to the activity stream, which could produce a big overhead I think.


Actually I had one idea now: I could "nerf" the Activities for the "private" stories by unsetting / setting to undefined the fields I don't want the client to receive (I don't want to publish the stories' ids if they are private).

But is there a straightforward way to do this? I couldn't think of any.

One other solution I could think of would be to combine cottz-publish-relations with some other server side reactivity package and use a tracker to rerun the publication when some criteria of them change - does anybody have a good pattern / experience with this?

Thanks and best wishes

Daniel

How handle array of objects in collection?

If you have a little time, please help me.

What is the correct way, if you have a field in collection which is array of objects?
My solution works, but not too elegant. The 'participants' field contains object array.
The code fragment is:

PublishRelations('MessagesList', function() {
  this.cursor(Collections.Messages.find({
    'participants.id': this.userId
  }, {
    sort: {
      createdAt: -1
    }
  }), function(id, doc) {
    var ids, participants;
    ids = [];
    participants = doc.participants;
    participants.forEach(function(participant) {
      return ids.push(participant.id);
    });
    this.cursor(Meteor.users.find({
      _id: {
        $in: ids
      }
    }), function() {});
    this.cursor(Collections.MessageItems.find({
      messageId: id
    }), function(id, doc) {});
  });
  return this.ready();
});

Thx
P.

this.join unexpected behaviour

So I first implemented reactive join using the example for the quick start you included in the README.md.

  this.cursor(Chatter.Room.find({ _id: roomId }, roomFilter), function (id, room) {
    this.cursor(Chatter.UserRoom.find({ roomId }, userRoomFilter), function (id, userRoom) {
      this.cursor(Meteor.users.find({ _id: userRoom.userId }, userFilter), function (id, user) {
      });
    });
  });

And this works marvellously. Then I try optimise this publish function using this.join() and this.send() like the following:

let users = this.join(Meteor.users, userFilter);
  let count = 0;
  this.cursor(Chatter.Room.find({ _id: roomId }, roomFilter), function (id, room) {
    this.cursor(Chatter.UserRoom.find({ roomId }, userRoomFilter), function (id, userRoom) {
      users.push(userRoom.userId);
    });
  });
  users.send();

And after adding and removing userRoom, which connect users and rooms, this version of the publication does not return the correct users. Am I missing something?

Thanks in advance for your help!

How to get doc for relations fields?

I tried

// Collection
Penalty:{
   _id : "001"
   ..........
}
Product:{
   .............
   penaltyId: "001"
}
-----------------------
// Server
PublishRelations('product', function (proId) {
    let penalty = this.join(Penalty);
    penalty.selector = function (_ids) {
        return {_id: _ids};
    };

    this.cursor(Product.find({_id: proId}), function (id, doc) {
            penalty.push(doc.penaltyId)
        }
    );

    penalty.send();

    return this.ready();
});
----------------------
// Router
FlowRoutes.route('/disbursement/:productId', {
    name: 'disbursement',
    title: 'Disbursement',
    subscriptions: function (params, queryParams) {
        this.register('product', Meteor.subscribe('product', params.productId));
    },
.............
// Client
...helpers({
    product(){
        let pro = Product.findOne(FlowRouter.getParam('productId');
        console.log(pro); // get product doc only (don't have penalty doc)
        return pro;
    },
})

How to find penalty doc relation?

Add support for server minimongo

When trying to publish from server-only minimongo, following error is shown:

TypeError: Cannot read property 'selector' of undefined
at HandlerController.add (packages/cottz:publish-relations/lib/server/handler_controller.js:22:34)
at CursorMethods.cursor (packages/cottz:publish-relations/lib/server/cursor/cursor.js:20:34)

Need help getting started

Suppose I have multiple collections, in this case, I have 3 collections I want to join.

Clicks: {
country: "anywhere",
agent_id: "someAgentId",
campaign_id: "someCampaignId",
}

Users //from Meteor.users collection
{
_id: "someUserId", --> this is the one attached as 'agent_id' to Clicks
roles: 'agent',
username: 'usernameHere'
}

Campaign: {
_id: "someCampaignId", --> attached as 'campaign_id' to Clicks
name: "someCampaignName"
}

Let's say I have 100 Clicks.

How Can I make it look like this in a table:

================================
country | agent | campaign

anywhere| Joe | GoogleCampaign
anywhere| John | BingCampaign
anywhere| Vic | FBCampaign
anywhere| Mike | TwitterCampaign

Notice that instead of displaying the 'agent_id', I want to display the name of the agent.
Just starting out with meteor and encountered this problem. Hope you could help me out.

the parent doc is removed, the related children doc is not removed.

this.cursor(Authors.find(authorId), function (id, doc) {
  this.cursor(Books.find({authorId: id}), function (id, doc) {
    this.cursor(Comments.find({bookId: id}));
  });
});

when i remove authorId1, the publish authorId1 is removed on client. but the published authorId1's books and comments to client not is removed.
please help me out. thank you!

Crossbar problem!

Hi Goluis

I just started using this package. And I got a lot of code reduce. Thank for making this great package.
Now I'm trying to use Crossbar API and I got error now

I20160531-03:12:50.404(9)? Exception while invoking method 'PR.fireListener' ReferenceError: Crossbar is not defined
I20160531-03:12:50.405(9)?     at [object Object].PRFireListener (packages/cottz:publish-relations/lib/server/methods.js:17:3)
I20160531-03:12:50.405(9)?     at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1704:12)
I20160531-03:12:50.405(9)?     at packages/ddp-server/livedata_server.js:711:19
I20160531-03:12:50.406(9)?     at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1)
I20160531-03:12:50.406(9)?     at packages/ddp-server/livedata_server.js:709:40
I20160531-03:12:50.406(9)?     at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1)
I20160531-03:12:50.406(9)?     at packages/ddp-server/livedata_server.js:707:46
I20160531-03:12:50.406(9)?     at tryCallTwo (/Users/woogenius/.meteor/packages/promise/.0.6.7.22t7ot++os+web.browser+web.cordova/npm/node_modules/meteor-promise/node_modules/promise/lib/core.js:45:5)
I20160531-03:12:50.406(9)?     at doResolve (/Users/woogenius/.meteor/packages/promise/.0.6.7.22t7ot++os+web.browser+web.cordova/npm/node_modules/meteor-promise/node_modules/promise/lib/core.js:200:13)
I20160531-03:12:50.406(9)?     at new Promise (/Users/woogenius/.meteor/packages/promise/.0.6.7.22t7ot++os+web.browser+web.cordova/npm/node_modules/meteor-promise/node_modules/promise/lib/core.js:66:3)

I think there's a typo on your package. I'll try to fix and make PR

WooJin.

one to many relationship

Hi,

Thanks for creating this package!

I tried different package for the relation but it seems that none of them wasn't able to solved my problem.

Here's the problem:
I have two collections, enrollments and claims
on my enrollments collection I have this fields:
fullName, products, childrenName, children2, children3, parents and siblings

on my claims collection I have this:

Principal - fullName from enrollments collection
Dependent-Children - childrenName, children2 and children3 from enrollments collection which is not an arrray, they are individual fields.

How do you setup the publish function for this scenario? Wherein the available childrenName is based on the enrollmentId field that the user select. Currently, all the childrenName is being populated from enrollments collection.

On my current set up on template claims form, I have select element which populated by fullName from my claims schema like this:

enrollmentId: {
    type: String,
    label: "Principal Name",
    autoform: {
      firstOption: 'Select Principal Name',
      options: function () {
        return Enrollments.find( {}, {
          sort: {
            fullName: 1
          }
        } ).map( function ( enrollmentId ) {
          return {
            label: enrollmentId.fullName,
            value: enrollmentId._id
          };
        } );
      },
      afFormGroup: {
        'formgroup-class': 'col-xs-6 col-sm-4'
      }
    }
  },

and here's on my children schema:

children: {
    type: String,
    label: "Dependent-Children",
    optional: true,
    autoform: {
      firstOption: 'Select Children',
      options: function () {
        var formId = AutoForm.getFormId();
        var enrollment = AutoForm.getFieldValue( 'enrollmentdId', formId );
        var child = Enrollments.find( {
          chidlrenName: enrollment
        }, {
          sort: {
            childrenName: 1
          }
        } ).map( function ( children1 ) {
          return {
            label: children1.childrenName,
            value: children1._id
          };
        } );
        console.log( child );
        return child;
      },
      afFormGroup: {
        'formgroup-class': 'col-xs-6 col-sm-4'
      }
    },
  },

enrollmentId is working as expected, the select element is populating with all the fullName field from enrollments collection but the the problem is on the childrenName it's populating all the children from other documents, it's supposed to be the children of the Princapal. I will referenced this thread for additional info.

Thanks in advance.

Changing filters reactively

First of all, the package looks great!

I know this is a weird error and I'll give you two very similar scripts, the first one works, the second doesn't.

In this case, the publication takes in the filters by which it searches the products.

PublishRelations('products', function(filters) {
  if (!this.userId || !Meteor.users.findOne(this.userId)) {
    return this.ready();
  }
  this.cursor(Products.find(filters));
  this.ready();
});

This works as expected.

In this other case, the filters get read from the user document. I update the user's filters via a Meteor.method and they are updated correctly. This publish gives the correct products to the client, but upon changing filters, those products that did much and now doesn't, don't get removed and also don't get updated reactively when they change.

PublishRelations('products', function() {
  if (!this.userId || !Meteor.users.findOne(this.userId)) {
    return this.ready();
  }
  this.cursor(Meteor.users.find(this.userId, { limit: 1 }), function(id, doc) {
    this.cursor(Products.find(doc.filters));
  });
  this.ready();
});

Everything gets fixed when refreshing the page so this looks related to the users collection. Does it behave differently from a normal collection?

Any ideas? Hope this helps.

Trying this.listen example

I'm trying to use the same approach as the example that uses this.listen but the publication throws Match failed upon startup. I can see that you do a Meteor.publish without arguments in the client so that may be throwing.

Have you tried your example? Does it work like this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.