Mongo sort result set


#1

Hi All,

I have the following Model. Made up of Chats that contain Messages.

models.d.ts

declare module 'api/models' {
  interface Chat {
    _id?: string;
    memberIds?: string[];
    title?: string;
    picture?: string;
    lastMessage?: Message;
    lastMessageCreatedAt?: Date;
    receiverComp?: Tracker.Computation;
    lastMessageComp?: Tracker.Computation;
  }

  interface Message {
    _id?: string;
    chatId?: string;
    senderId?: string;
    ownership?: string;
    content?: string;
    createdAt?: Date;
  }
}

I am retrieving the Chats as follows:

        const chats: Mongo.Cursor<Chat> = Chats.find({
          memberIds: this.senderId
        }, {
            transform: this.transformChat.bind(this),
            // sort: {
            //   lastMessage: {createdAt: -1}
            // },
            fields: {
              memberIds: 1
            }
          });

Question

How do I sort the Chats result set by the createdAt of its last Messages?

More info

After the result set is obtained, the lastMessage is set. This is done in the transformChat function seen being invoked above (transform: this.transformChat.bind(this)).

chat.lastMessage = this.findLastMessage(chat);

  private findLastMessage(chat: Chat): Message {
    return Messages.findOne({
      chatId: chat._id
    }, {
        sort: { createdAt: -1 }
      });
  }

It’s after this I think, that I need to sort the Chats result set.


#2

This suggests that a Mongo.Cursor has a sort method. However, how do you do this on the Mongo.Cursor typscript/javascript object?


#3

The minimongo cursor object does not contain a sort method.

Have you considered publishing the “joined” documents in the first place? https://atmospherejs.com/reywood/publish-composite


#4

I added the following pubsub set up. It works perfectly. However, when I add the commented out section (sort: { lastMessageCreatedAt: -1 }) below, then it does not sync the server database with the mini mongo.

publications.ts

import {Meteor} from 'meteor/meteor';
import {Mongo} from 'meteor/mongo';
import {Chat, Message} from 'api/models';
import {Chats, Messages} from './collections';

Meteor.publishComposite('chats', function (senderId: string): PublishCompositeConfig<Chat> {
    if (!senderId) return;

    return {
        find: () => {
            return Chats.find({
                memberIds: senderId//,
                //sort: { lastMessageCreatedAt: -1 }
            });
        },

        children: [
            <PublishCompositeConfig1<Chat, Message>>{
                find: (chat) => {
                    return Messages.find({ chatId: chat._id }, {
                        sort: { createdAt: -1 },
                        limit: 1
                    });
                }
            }
        ]
    };
});

Meteor.publish('messages', function (chatId: string, senderId: string): Mongo.Cursor<Message> {
    if (!senderId) return;
    if (!chatId) return;

    return Messages.find({ chatId });
});

chats.ts

    let chats: Mongo.Cursor<Chat> = null;
    this.subscribe('chats', this.senderId, () => {
        chats = this.findChats();
   });

and

  private findChats(): Mongo.Cursor<Chat> {
    const chats: Mongo.Cursor<Chat> = Chats.find({
      memberIds: this.senderId
    }, {
        transform: this.transformChat.bind(this),
        //sort: { lastMessageCreatedAt: -1 },
        fields: {
          memberIds: 1
        }
      });
    return chats;
  }

#5

That should be

return Chats.find({ memberIds: senderId }, { sort: { lastMessageCreatedAt: -1 } });

(Separate query and options objects).


#6

Thanks Rob,

When I make the changes you suggest to publications.ts, it works now, returning the result set, but it is not sorted.

So I figure, I also need to implement the changes you suggest to chats.ts. Is that correct?

If I make the changes below, I get the following error in the Ionic CLI at build time:

ERROR in ./app/pages/chats/chats.ts
(195,39): error TS2346: Supplied parameters do not match any signature of call target.

  private findChats(): Mongo.Cursor<Chat> {
    const chats: Mongo.Cursor<Chat> = Chats.find(
      { memberIds: this.senderId },
      { sort: { lastMessageCreatedAt: -1 } },
      {
        transform: this.transformChat.bind(this),
        fields: { memberIds: 1 }
      }
    );
    return chats;
  }

This works if I remove this line:

{ sort: { lastMessageCreatedAt: -1 } },

Should I consider copying the Mongo.Cursor<Chat> data to an array, and sort it myself? Or is Mongo supposed to assist with the sorting? (I would have guessed that latter).

Appreciate your help.


#7

You’ve added an extra object to the find. It should look like:

private findChats(): Mongo.Cursor<Chat> {
    const chats: Mongo.Cursor<Chat> = Chats.find(
      { memberIds: this.senderId },
      { sort: { lastMessageCreatedAt: -1 },
        transform: this.transformChat.bind(this),
        fields: { memberIds: 1 }
      }
    );
    return chats;
  }

#8

Thanks Rob.

I tried that, I get no errors now, and I get the result set back. But it is not sorted.

Do you have any ideas?


#9

If you mean on the client, it is possible that the order in minimongo does not match that of the server-side cursor. The recommended approach is to repeat the find used on the server within the client.


#10

As far as I can see, the client and server are both sorting on { lastMessageCreatedAt: -1 }. As a result I would expect to see the rows sorted on the client.

Am I doing something wrong?

On the client I do:

chats.ts

private findChats(): Mongo.Cursor<Chat> {
    const chats: Mongo.Cursor<Chat> = Chats.find(
      { memberIds: this.senderId },
      { sort: { lastMessageCreatedAt: -1 },
        transform: this.transformChat.bind(this),
        fields: { memberIds: 1 }
      }
    );
    return chats;
  }

and on the server:

publications.ts

import {Meteor} from 'meteor/meteor';
import {Mongo} from 'meteor/mongo';
import {Chat, Message} from 'api/models';
import {Chats, Messages} from './collections';

Meteor.publishComposite('chats', function (senderId: string): PublishCompositeConfig<Chat> {
    if (!senderId) return;

    return {
        find: () => {
            return Chats.find(
                { memberIds: senderId },
                { sort: { lastMessageCreatedAt: -1 } }
            );
        },

        children: [
            <PublishCompositeConfig1<Chat, Message>>{
                find: (chat) => {
                    return Messages.find({ chatId: chat._id }, {
                        sort: { createdAt: -1 },
                        limit: 1
                    });
                }
            }
        ]
    };
});

Meteor.publish('messages', function (chatId: string, senderId: string): Mongo.Cursor<Message> {
    if (!senderId) return;
    if (!chatId) return;

    return Messages.find({ chatId });
});

#11

I’ve either misunderstood or can’t see the wood for the trees.

So I’m clutching at straws now. You are subscribing to the chats publication on the client and have removed the autopublish package? (The only way your client Chats collection can get its data is from the publishComposite).


#12

You know your stuff!!

I am using this, and did the following:

meteor remove autopublish

then

meteor add reywood:publish-composite
typings install dt~meteor-publish-composite --save --global


#13

If you’ve removed autopublish, then the only way to get published data on the client is through a subscription - so do you have any other subscriptions which may be “corrupting” what you expect to see?

Can we see what you see (object ideally or screenshot), just to understand the data a little better?


#14

The only subscription I have is to 'chats'. Which I have now moved to a Promise. This is the only way I ever try retrieve the chats. I get them successfully, but just not sorted as expected.

  private findChats(senderId: string): Promise<Mongo.Cursor<Chat>> {
    let promise: Promise<Mongo.Cursor<Chat>> = new Promise<Mongo.Cursor<Chat>>(resolve => {
      this.subscribe('chats', this.senderId, () => {
        const chats: Mongo.Cursor<Chat> = Chats.find(
          { memberIds: this.senderId },
          {
            sort: { lastMessageCreatedAt: -1 },
            transform: this.transformChat.bind(this),
            fields: { memberIds: 1 }
          }
        );
        resolve(chats);
      });
    });
    return promise;
  }

I don’t see any errors, I am sure there aren’t any. Here I have printed out the chats object. (I am not sure what more info you want?)

_selectorId
	
	undefined
collection
	
	Object { name="chats",  _docs={...},  _observeQueue={...},  more...}
fields
	
	Object { memberIds=1}
limit
	
	undefined
matcher
	
	Object { _paths={...},  _hasGeoQuery=false,  _hasWhere=false,  more...}
reactive
	
	true
skip
	
	undefined
sorter
	
	Object { _sortSpecParts=[1],  _sortFunction=null,  _keyComparator=function(),  more...}
_depend
	
	function(changers, _allow_unordered)
_getCollectionName
	
	function()
_getRawObjects
	
	function(options)
_projectionFn
	
	function(obj)
_publishCursor
	
	function(sub)
_transform
	
	function(doc)
count
	
	function()
fetch
	
	function()
forEach
	
	function(callback, thisArg)
getTransform
	
	function()
map
	
	function(callback, thisArg)
observe
	
	function(callbacks)
observeChanges
	
	function(callbacks)
rewind
	
	function()
__proto__
	
	Object { rewind=function(),  forEach=function(),  getTransform=function(),  more...}

the sorter

sorter
	
	Object { _sortSpecParts=[1],  _sortFunction=null,  _keyComparator=function(),  more...}
_keyFilter
	
	null
_sortFunction
	
	null
_sortSpecParts
	
	[Object { path="lastMessageCreatedAt",  ascending=false,  lookup=function()}]
0
	
	Object { path="lastMessageCreatedAt",  ascending=false,  lookup=function()}
ascending
	
	false
path
	
	"lastMessageCreatedAt"
lookup
	
	function(doc, arrayIndices)

chat.lastMessageCreatedAt is populated for each chat element.


#15

I was interested in the documents being returned - what do they look like? So, console.log(JSON.stringify(Chats.find().fetch(), null, 2)); unless that’s too much data to copy/paste.


#16
[
  {
    "_id": "6LGkfcY3r2boGom6f",
    "memberIds": [
      "P9",
      "J65"
    ]
  }
]

#17

So how do you know that it’s not being sorted by lastMessageCreatedAt if there’s only one document?


#18

Sorry, here is with 3 chats:

[
{
"_id": “6LGkfcY3r2boGom6f”,
“memberIds”: [
“P9”,
“J65”
]
},
{
"_id": “CFzeMdX8uBfL935fo”,
“memberIds”: [
“P9”,
“J66”
]
},
{
"_id": “pEQozdWWi54A4tCq6”,
“memberIds”: [
“P9”,
“J64”
]
}
]


#19

Ah - you don’t include the lastMessageCreatedAt, so the client cannot do the sort.

I should have seen that sooner from your publication.


#20

I have the following in my typings on both server and client:

As you can see, it does have lastMessageCreatedAt. Where else do I need to have it?

declare module 'api/models' {
  interface Chat {
    _id?: string;
    memberIds?: string[];
    title?: string;
    picture?: string;
    lastMessage?: Message;
    lastMessageCreatedAt?: Date;
    receiverComp?: Tracker.Computation;
    lastMessageComp?: Tracker.Computation;
  }

  interface Message {
    _id?: string;
    chatId?: string;
    senderId?: string;
    ownership?: string;
    content?: string;
    createdAt?: Date;
  }
}