Complicated search in MongoDB - mongodb

I have defined a PostSchema as follows. A post is written by an author, and can be read by many people: lastOpens is an array of { time: ... , userId: ... }.
var PostSchema = new mongoose.Schema({
title: { type: String }
author: { type: mongoose.Schema.Types.ObjectId, ref: 'user' },
lastOpens: { type: Array, default: [] }
})
Now, I want to write a static method that returns all the posts read by one user:
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
// need to go through all the posts, and check their `lastOpens`.
// If `userId` is in `userId` of a `lastOpen`, then count the post in
}
What I know is the methods like find({ ... }) of MongoDB. But I don't know how to specify a more complicated search like mine.
Could anyone help?
Edit 1: I tried to use $where operator as follows, it did not work:
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
return this.find({ $where: function () {
var index = -1;
for (var i = 0; i < this.lastOpens.length; i++)
if (this.lastOpens[i].userId === userId) { index = i; break }
return !(index === -1)
}}, cb)
Is there anything we could not do inside $where?

You can use Mongo's query an array of embedded documents.
In your case it will look something like :
PostSchema.statics.postsOpenedByUser = function (userId, cb) {
return this.find( { "lastOpens.userId" : userId }, cb );
}
This will return all posts that have userId in the lastOpens

Related

MongoDB query - pass the function to the Model.find()

I have issue with querying MongoDB (Mongoose) by passing the function as parameter in Model.find() -> like this Model.find(searchCondition). I hope that you can help me.
// Fetching patients from the database
exports.getPatients = (req, res, next) => {
const criterionSearchCategory = req.query.kriterijumPretrage;
const ageSearchCategory = req.query.kriterijumGodina;
const searchInputValue = req.query.pojamPretrage;
console.log({ [criterionSearchCategory]: { [ageSearchCategory]: Number([searchInputValue]) }});
// Patient search condition, based on selected option from select dropdown
function searchCondition() {
if (criterionSearchCategory == 'undefined') {
return {};
} else if (criterionSearchCategory == 'age') {
return { [criterionSearchCategory]: { [ageSearchCategory] : Number([searchInputValue]) }}
} else {
return { [criterionSearchCategory]: { $in: [ "/^" + searchInputValue + "/i" ]}}
}
}
...
const patientQuery = Patient.find(searchCondition);
getPatients(patientsPerPage: number, currentPage: number, criterionSearchCategory: string, searchInputValue: string, ageSearchCategory: any) {
const queryParams = `?pacijenataPoStranici=${patientsPerPage}&trenutnaStranica=${currentPage}&kriterijumPretrage=${criterionSearchCategory}&pojamPretrage=${searchInputValue}&kriterijumGodina=${ageSearchCategory}`;
this.http
.get<{ message: string, patients: any, maxPatients: number }>( BACKEND_URL + queryParams)
// Execute map on every data that makes it through Observable stream
.pipe(map((patientData) => {
I want to menton when I pass the query params manually, for example const patientQuery = Patient.find({ age: { '$gt': 30 } }); appropriate patients will be fetched correctly , but when I pass the function , like this const patientQuery = Patient.find(searchCondition); then does not work.
The first question, is it possible to pass the function as parameter like this?
Any suggestion will be appreciate. Thank you

feathers-mongodb Service.find({query: {_id}}) returns null

I have the schemas below:
students.graphql.schema.js
export default [
`
type StudentsWithPagination {
total: Int
items: [Students]
}
type Students {
_id: String!
name: String
address: Addresses
}
`,
];
addresses.graphql.schema.js
export default [
`
type AddressesWithPagination {
total: Int
items: [Addresses]
}
type Addresses {
_id: String!
title: String
}
`,
];
I have created two services by running feathers generate service students.service.js and addresses.services.js.
When I search addresses by title, I get result. However, when I search by _id, I get null. Something like:
const studentsResolvers = {
Students: {
address: student => {
const query = {
_id: student.address
}
return Addresses.find({ query }).then(result => {
console.log(result)
})
}
}
}
The code above produces null though student.address returns the right address._id. I still get null even I hardcode student.address with the right address._id
The code above will return null unless I search by address title. Something like:
const query = {
title: 'my-location'
}
_id is of type String, not ObjectID.
What am I doing wrong?
As documented in the feathers-mongodb adapter, since MongoDB itself (unlike Mongoose) does not have a schema, all query parameters have to be converted to the type in the database in a hook manually. The example can be adapted accordingly for $in queries:
const ObjectID = require('mongodb').ObjectID;
app.service('users').hooks({
before: {
find(context) {
const { query = {} } = context.params;
if(query._id) {
query._id = new ObjectID(query._id);
}
if(query.age !== undefined) {
query.age = parseInt(query.age, 10);
}
context.params.query = query;
return Promise.resolve(context);
}
}
});

How to replace a manual id with an ObjectID _id in mongoDB?

Let's say I have a database with two collections, kids and classes. Each kid belongs to one class.
Each class has a previously created integer id.
I want to replace the kid.class_id with the (ObjectID) _id of the class, not the (integer) id of the class.
However, when I run the script below, it doesn't reset the class_id with the class._id -- it remains the old integer id.
mongoose.connect(someMongodbUri, { useMongoClient: true }, (err, db) => {
let kidsCount = 0;
db.collection('kids').find({}).each((err, kid) => {
kidsCount++;
db.collection('classes')
.findOne({ id: kid.class_id })
.then((class, err) => {
let newClassId = class._id;
db.collection('kids').updateOne(
{ _id: kid._id },
{ $set: { class_id: newClassId } }
).then(() => {
console.info('Updated', kid.class_id);
kidsCount--;
if (kidsCount === 0) { db.close(); }
});
});
});
});
Am I missing something? Thanks for any help you can offer!
We can convert integerId to Object id.
var ObjectId = require('mongodb').ObjectID;
let newClassId = ObjectId(class._id);
There may be better or elegent ways that i don't know, but this works for me.

Recursive Publishing - Angular-meteor

I am wanting to load a topic, 25 of its comments and up to 5 sub comments for each comment, repeated recursively over each comment/sub-comment until all related comments are found.
I'm currently using an angular directive to recursively subscribe and add to the local collection whenever the comment has children. It works quite well, but there is some lag (to be expected) between loading the initial 25 comments, and loading their children, then their children and so on.
This issue isn't a problem when just loading a page at a time. It becomes an issue when using infinite scrolling and increasing that initial 25 comment limit. It will cause the page to jump up and down a bit as the sub comments disappear and reappear once loaded again.
I was wondering how I could recursively look up all comments prior to sending to the local client so I don't need to make more than one round trip for each topic.
I have a demo loaded up at ck-gaming.com
If you scroll to the bottom it will load more and you'll see it jump all over as the sub comments are reloaded into the page.
The two options I can think of would be to use a resolve to wait for all collections prior to loading the page or using recursive publish to get them all first.
Thoughts? Ideas?
Ok, my first attempt that I would like some thoughts on if possible.
For the publishing I decided to go with publish-composite to make publishing from the same collection easier.
for the publication I wrote:
Meteor.publishComposite('oneDiscussion', function (slug, options) {
var query = {};
query.find = function () {
return Discussions.find({ slug: slug }, { limit: 1 });
};
var mainChildQuery = Comments.find({ slug: slug }, { limit: 1 });
query.children = [];
query.children[0] = {};
query.children[0].find = function (discussion) {
return mainChildQuery;
};
query.children[0].children = [];
query.children[0].children[0] = {};
query.children[0].children[0].find = function (comment) {
return Meteor.users.find({ _id: comment.author.id }, { limit: 1, fields: { profile: 1, roles: 1, createdAt: 1, username: 1 } });
};
query.children[0].children[1] = {};
query.children[0].children[1].find = function (parent) {
Counts.publish(this, 'numberOfComments', Comments.find(
{ parent_id: parent._id }
), { noReady: true });
console.log(options)
return Comments.find({ parent_id: parent._id }, options);
};
// var parentQuery = Comments.find({ slug: slug });
var parent = mainChildQuery.fetch();
var children = Comments.find({ parent_id: parent[0]._id }, { limit: 25 }).fetch();
var childrenIds = _.pluck(children, '_id');
var getChildren = function (children_ids, thisParent) {
var i = 0;
thisParent.children = [];
var recursive = function getEm(children, parent) {
_.each(children, function (id) {
// parent.children[i] = new Children(id);
var query = Comments.find({ parent_id: id }, { limit: 5, sort: { date: -1 } });
parent.children[i] = {
find: function () {
return Comments.find({ parent_id: id }, { limit: 5, sort: { date: -1 } });
}
};
var children1 = query.fetch();
var newChildrenIds = _.pluck(children1, '_id');
i++;
if (newChildrenIds.length > 0) {
getEm(newChildrenIds, parent);
}
});
}
recursive(children_ids, thisParent);
};
getChildren(childrenIds, query.children[0].children[1]);
return query;
});
Seems to be working ok so far, though running it on my desktop it's not as performant as I would think it should be. I'll deploy it and see if there's a difference online. I'll update when I get home and can update the live site. If anyone can find something wrong with what I've written it would be much appreciated.
I've came up with what I think is the best solution. I improved on the function above and so far I'm really enjoying the results.
Here is the publish function:
Meteor.publishComposite('comments', function (item_id, options) {
/**
* TODO: Add query to find a user for each comment.
*/
/**
* Start building our query.
* Add the latest 25 (depending on options) child comments of the viewed item
* to the query.
*/
var query = {
find: function () {
return Comments.find({ parent_id: item_id }, options);
}
};
// Query the database for the first 25? comments, we'll need their _id's
var mainChildren = Comments.find({ parent_id: item_id }, options).fetch();
// pluck the id's from the initial comments
var mainChildrenIds = _.pluck(mainChildren, '_id');
/**
* Builds the remaining query based on the id's plucked from the children
* above.
* #param childrens_id The id's we just plucked from the above query
* #param thisParent This is the parent query
*/
var getChildren = function (children_ids, parentQuery) {
// initiate i to 0
var i = 0;
// add a child array to the current parent query.
parentQuery.children = [];
var recursive = function getem(children, parent) {
_.each(children, function (id) {
var query = Comments.find({ parent_id: id }, { limit: 5, sort: { date: 1 } });
parent.children[i] = {
find: function () {
return query;
}
};
var children1 = query.fetch();
var newChildrenIds = _.pluck(children1, '_id');
i++;
if (newChildrenIds.length > 0) {
getem(newChildrenIds, parent);
}
});
};
// recursively build the query if there are children found.
recursive(children_ids, parentQuery);
};
// initiate the query build function
getChildren(mainChildrenIds, query);
return query;
});
I created an example app you can get on GitHub here
And you can view it running on meteorpad here
what it does
All the function does is build the publishComposite query, recursively looping over the children id's, as long as there are children id's. When there are no more children it stops.
to use it
You want a collection of comments (or whatever you're nesting) that have a parent_id field. This field will be filled with either the parent post Id, parent Item id (if say making a store with reviews/comments). The parent post id would of course be the comment or post you are commenting on. See the example for more information.

Using $inc to increment a document property with Mongoose

I would like to increment the views count by 1 each time my document is accessed. So far, my code is:
Document
.find({})
.sort('date', -1)
.limit(limit)
.exec();
Where does $inc fit in here?
Never used mongoose but quickly looking over the docs here it seems like this will work for you:
# create query conditions and update variables
var conditions = { },
update = { $inc: { views: 1 }};
# update documents matching condition
Model.update(conditions, update).limit(limit).sort('date', -1).exec();
Cheers and good luck!
I ran into another problem, which is kind of related to $inc.. So I'll post it here as it might help somebody else. I have the following code:
var Schema = require('models/schema.js');
var exports = module.exports = {};
exports.increase = function(id, key, amount, callback){
Schema.findByIdAndUpdate(id, { $inc: { key: amount }}, function(err, data){
//error handling
}
}
from a different module I would call something like
var saver = require('./saver.js');
saver.increase('555f49f1f9e81ecaf14f4748', 'counter', 1, function(err,data){
//error handling
}
However, this would not increase the desired counter. Apparently it is not allowed to directly pass the key into the update object. This has something to do with the syntax for string literals in object field names. The solution was to define the update object like this:
exports.increase = function(id, key, amount, callback){
var update = {};
update['$inc'] = {};
update['$inc'][key] = amount;
Schema.findByIdAndUpdate(id, update, function(err, data){
//error handling
}
}
Works for me (mongoose 5.7)
blogRouter.put("/:id", async (request, response) => {
try {
const updatedBlog = await Blog.findByIdAndUpdate(
request.params.id,
{
$inc: { likes: 1 }
},
{ new: true } //to return the new document
);
response.json(updatedBlog);
} catch (error) {
response.status(400).end();
}
});