MongoDB aggregate project based on conditional logic of other field - javascript

Let's say I have a Schema with a couple fields foo and bar;
I then want to retrieve all Documents using a projection. I want to retrieve all foos and bars with aliases and then "create" another field for my result based on some conditional logic of what bar is. If the condition is true, I simply want to tack on a leading '0' char to this new field, otherwise, I just want to set it to whatever barAlias is.
So, something like
const pipeline = [
{ $match: {} },
{
$project: {
fooAlias: "$foo",
barAlias: "$bar",
newField: (if some condition with barAlias) ? '0' + barAlias : barAlias
}
}
];
const docs = await Collection.aggregate(pipeline);
I know how to use $cond and $concat, but my issue here is that I'm trying to base my logic on the alias fields. Is this possible. Thanks!

Related

resolve field function before sort

I am creating a graphql server using express, and I have a resolver that can transform my fields as per input from the user query.
The transformer that I am using is returning a function, which is the cause of my issues.
I want to sort my result by some user determined field, but since the field is a function, it won't work.
So the resolver looks like this:
const resolver = (req, param) => {
return {
history: async input => {
let size = input.pageSize || 3;
let start = (input.page || 0) * size;
let end = start + size;
let sortField = (input.sort || {}).field || 'timestamp';
return fs.promises.readFile("./history/blitz.json", "utf8").then(data =>
JSON.parse(data)
.slice(start, end)
.map(job => historyTransformer(job))
.sort((a,b) => a[sortField] > b[sortField] ? 1 : a[sortField] < b[sortField] ? -1 : 0)
);
}
};
};
and the transformer:
const historyTransformer = job => {
return {
...job,
timestamp: input =>
dateFormat(job.timestamp, input.format || "mm:hh dd-mm-yyyy")
};
};
I am not sure if I am missing something but is there an easy way of resolving the function call before starting the sorting?
GraphQL fields are resolved in a hierarchal manner, such that the history field has to resolve before any of its child fields (like timestamp) can be resolved. If the child field's resolver transforms the underlying property and your intent is to somehow use that value in the parent resolver (in this case, to do some sorting), that's tricky because you're working against the execution flow.
Because you're working with dates, you should consider whether the format of the field even matters. As a user, if I sort by timestamp, I expect the results to be sorted chronologically. Even if the response is formatted to put the time first, I probably don't want dates with the same times but different years grouped together. Of course, I don't know your business requirements and it still doesn't solve the problem if we're working with something else, like translations, which would cause the same problem.
There's two solutions I can think of:
Update your schema and lift the format argument into the parent field. This is easier to implement, but obviously not as nice as putting the argument on the field it applies to.
Keep the argument where it is, but parse the info parameter passed to the resolver to determine the value of the argument inside the parent resolver. This way, you can keep the argument on the child field, but move the actual formatting logic into the parent resolver.

JavaScript: Developing a search query for MongoDB

I need some assistance developing a search query to give a button in my Electron application some functionality.
This is how far I have gotten:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
Artist.sort()
.find()
.offset()
.limit();
};
I am skipping criteria for now so feel free to ignore that. The user should be able to sort by artist name, age and albums released. The sortProperty will go in ascending fashion so I know I need to sort with a value of 1.
The real challenge behind the sorting is that I need to specify different sortProperties but only one at any given time.
I want to be able to pass in a different sortProperty depending on what the user has selected. I would like to have a key that is whatever the sortProperty is.
So if sortProperty is equal to age, I need to ensure I have a statement that says age.
It seems you already have all the required parts to do the query. You just need to create the sort selector based on your sortProperty. This will be an object with a key equal to the value held by sortProperty for example age. The result will look like this
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
return Artist
.find()
.sort({ [sortProperty]: 1 })
.skip(offset)
.limit(limit);
};
Note
To illustrate the dynamic key assignation, here's a snippet
const sortProperty = 'age';
const selector = { [sortProperty]: 1 };
console.log(selector);
So the above answer is the best solution as it is ES6 and more elegant. I wanted to share the ES5 old school way that I learned after I got this answer and studied it more in-depth.
In the ES5 way I learned that you can define an object like so:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
const sortOrder = {};
Artist.find({})
.sort()
.skip(offset)
.limit(limit);
};
I made it an empty object and then look at it and add sortProperty equals 1 like so:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
const sortOrder = {};
sortOrder[sortProperty] = 1;
Artist.find({})
.sort()
.skip(offset)
.limit(limit);
};
And then I can pass in the sortOrder like so:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
const sortOrder = {};
sortOrder[sortProperty] = 1;
Artist.find({})
.sort(sortOrder)
.skip(offset)
.limit(limit);
};
Okay, so that is the ES5 approach if anyone was interested in learning about it, but as you can see above the ES6 solution looks better.
The whole problem with creating an object and simultaneously adding a property to it is that its not well supported in ES5, we have to first declare an object, then add a property to it and set that property equal to 1.
The square brackets mean look at the sortProperty variable:
sortOrder[sortProperty] = 1;
which is a string and I am trying to add on a sortOrder property of name to this object and set it equal to 1 and so the end result if you ran this in a Chrome console or in code snippet is { name: 1 }.
The learning curve for me and what the selected answer helped me learn was ES6 interpolated properties or keys. This thing:
.sort({ [sortProperty]: 1 })
What is happening here is at runtime look at the sortProperty variable and whatever its equal to, add that property to this object and give it a value of 1.
So to be clear, yes the original answer given was the elegant solution I went with:
module.exports = (criteria, sortProperty, offset = 0, limit = 20) => {
// write a query that will follow sort, offset, limit options only
// do not worry about criteria yet
Artist.find()
.sort({ [sortProperty]: 1 })
.skip(offset)
.limit(limit);
};
I was just sharing what I learned along the way in studying and implementing it.

How do I query MongoDB for 2 ranges and text search?

I have event objects in MonogDB that look like this:
{
"start": 2010-09-04T16:54:11.216Z,
"title":"Short descriptive title",
"description":"Full description",
"score":56
}
And I need to get a query across three parameters:
Time window (event start is between two dates)
Score threshold (score is > x)
Full-text search of title and description
What's the right way to approach this efficiently? I think the first two are done with an aggregation but I'm not sure how text search would factor in.
Assuming your start field is of type date (which it should be) and not a string, here are the basic components that you'd want to play with. In fact, given the ISO 8601 structure of a MongoDB date a string based comparison would work just as well.
// create your text index
db.collection.ensureIndex({
description: "text",
title: "text"
})
// optionally create an index on your other fields
db.collection.ensureIndex({
start: 1,
score: 1
})
x = 50
lowerDate = ISODate("2010-09-04T16:54:11.216Z") // or just the string part for string fields
upperDate = ISODate("2010-09-04T16:54:11.216Z")
// simple find to retrieve your result set
db.collection.find({
start: {
$gte: lowerDate, // depending on your exact scenario, you'd need to use $gt
$lte: upperDate // depending on your exact scenario, you'd need to use $lt
},
score: { $gt: x }, // depending on your exact scenario, you'd need to use $gte
$text: { // here comes the text search
$search: "descriptive"
}
})
There is an important topic with respect to performance/indexing that needs to be understood, though, which is very well documented here: Combine full text with other index
This is why I initially wrote "components of what you'd want to play with". So depending on the rest of your application you may want to create different indexes.

Create/update objects with mongoose/mongoDB

The internet is full of resources for dealing with arrays, but often objects are a more natural fit for data and seemingly more efficient.
I want to store key-value objects under dynamic field names like this:
project['en-US'] = { 'nav-back': 'Go back', ... }
project['pt-BR'] = { 'nav-back': 'Volte', ... }
Doing this seems like it would be more efficient than keeping an array of all languages and having to filter it to get all language entries for a given language.
My question is: How can I insert a key-value pair into an object with a dynamic name using mongoose? And would the object need to exist or can I create it if it doesn't in one operation?
I tried this:
await Project.update(
{ _id: projectId },
{
$set: {
[`${language}.${key}`]: value,
},
});
But no luck regardless of if I have an empty object there to begin with or not: { ok: 0, n: 0, nModified: 0 }.
Bonus: Should I index these objects and how? (I will want to update single items)
Thanks!
In mongoose, the schema is everything. It describe the data you gonna read/store from the database. If you wanna add dynamically a new key in the schema it's gonna be hard.
In this particulary case I would recommend to use the mongodb-native-driver which is way more permissive about the data manipulation. So you could read the data in a specific format and dynamically add your field into it.
To resume my thought, how should your dynamic change happen :
Use mongodb-native-driver to insert the new key into the database data
Modify the mongoose schema you have in the code (push a new key into it)
Use mongoose to manipulate the data afterward
Do not forget to dynamically update your mongoose model or you won't read the new key at the next find.
I solved this using the original code snippet unchanged, but adding { strict: false } to the schema:
const projectSchema = new Schema({ ...schema... }, { strict: false });

Mongodb create pseudo fields?

Hello I'd like to know if it's possible to create pseudo properties on Mondodb. This is, currently I have a collections users like this:
{_id: (_1), name: "user1", secret: "1"}
{_id: (_2), name: "user2", secret: "2"}
When I query the database. I do something like:
function getuser(objectId) {
db.users.find({_id : objectId}).toArray(function(err, result) {
x = result[0];
x.pseudoField1 = hash(secret);
return x;
});
}
Then I do some operations on the x object, and return to put on the database, but before I have to filter the not needed properties, so I do:
y = {}
y._id = x._id
y.name = x.name
y.secret = x.secret
db.users.update({_id: y._id}, y);
What I'd like to do is know if there is any way to make the databse automaticaly return an object with the pseudoField1 with the function I want, and furthermore, when I issue an update with x, only the fields _id, name and secret get updated.
When you want to calculate fields on the database, you can use an aggregation pipeline with a $project stage. The aggregation frameworks offers some simple arithmetic operators to create fields which are derived from values of other fields, but implementing a complex hash function is likely far too complicated to do on the database.
Your second requirement - telling MongoDB to ignore a certain field when inserting - isn't possible out-of-the-box. But what you can do is remove the field from the document before saving it. You can also use an object-document wrapper like Mongoose which allows you to define schemas and exclude certain fields from storing them in the database.

Categories