Mongoose: Sort Documents based on array properties - node.js

Good morning/afternoon.
I'm trying to sort out documents based on multiple array properties to make a "Ranking" system.
The user Schema that i want to sort:
const UserSchema = new mongoose.Schema(
{
// ... Rest of the code
modules: {
party: Array,
bank: Array,
// ... Rest of the code
},
{ strict: false }
)
Which every new 'partner' added to the user, pushes This to the modules.bank (or party):
{
_id: "some_randomgenerated_id",
id: "id of the monster on the database",
level: Number,
exp: Number,
stats: {
attack: Number
defense: Number,
additional: {
attack: Number,
defense: Number
}
}
I want to sort the Users based on each partner's streght combined, like partner[0].stats.attack + .defense + (additional stats) and so on...
I Tried using aggregate, but haven't got too far with it.

Related

Storing and querying JSON arrays in Redisjson with nodejs

What I was hoping to do was store an array of objects using RedisJSON very simply and then query that array.
I have something similar to this:
const data = [
{
_id: '63e7d1d85ad7e2f69df8ed6e',
artist: {
genre: 'rock',
},
},
{
_id: '63e7d1d85ad7e2f69df8ed6f',
artist: {
genre: 'metal',
},
},
{
_id: '63e7d1d85ad7e2f69df8ed6g',
artist: {
genre: 'rock',
},
},
]
then I can easily store and retrieve this:
await redisClient.json.set(cacheKey, '$', data)
await redisClient.json.get(cacheKey)
works great. but now I want to also query this data, I've tried creating an index as below:
await redisClient.ft.create(
`idx:gigs`,
{
'$.[0].artist.genre': {
type: SchemaFieldTypes.TEXT,
AS: 'genre',
},
},
{
ON: 'JSON',
PREFIX: 'GIGS',
}
)
and when I try and search this index what I expect is it to return the 2 documents with the correct search filter, but instead it always returns the entire array:
const searchResult = await redisClient.ft.search(`idx:gigs`, '#genre:(rock)')
produces:
{
total: 1,
documents: [
{ id: 'cacheKey', value: [Array] }
]
}
I can't quite work out at which level I'm getting this wrong, but any help would be greatly appreciated.
Is it possible to store an array of objects and then search the nested objects for nested values with RedisJSON?
The Search capability in Redis stack treats each key containing a JSON document as a separate search index entry. I think what you are doing is perhaps storing your whole array of documents in a single Redis key, which means any matches will return the document at that key which contains all of your data.
I would suggest that you store each object in your data array as its own key in Redis. Make sure that these will be indexed by using the GIGS prefix in the key name, for example GIGS:63e7d1d85ad7e2f69df8ed6e and GIGS:63e7d1d85ad7e2f69df8ed6f.
You'd want to change your index definition to account for each document being an object too so it would look something like this:
await redisClient.ft.create(
`idx:gigs`,
{
'$.artist.genre': {
type: SchemaFieldTypes.TEXT,
AS: 'genre',
},
},
{
ON: 'JSON',
PREFIX: 'GIGS:',
}
)
Note I also updated your PREFIX to be GIGS: not GIGS - this isn't strictly necessary, but does stop your index from accidentally looking at other keys in Redis whose name begins GIGS<whatever other characters>.

Unique key only in array of subschemas?

I have a schemas. One of its keys takes the type of an array of schemas.
let instanceSchema = new Mongoose.Schema({
name: { type: String, required: true, unique: true }
})
let mainSchema = new Mongoose.Schema({
instances: { type: [instanceSchema], required: true }
})
So the mainSchema has a key that is an array of instanceSchemas. I want the behavior to be such that I can have instanceSchemas with the same name if they are members of different mainSchemas, like so:
let MainModel = mongoose.model("MainModel", mainSchema);
// Succeeds
main1 = new MainModel({
"instances": [ {"name": "Instance1"}, {"name": "Instance2"} ];
});
// Succeeds
main2 = new MainModel({
"instances": [ {"name": "Instance1"}, {"name": "Instance2"} ];
});
As shown, we have two named "Instance1" and two named "Instance2" but they are members of different documents (main1 and main2) so my target behavior is that this should be allowed. However, using unique prevents this from happening as Mongoose checks all instanceSchema models. Is there a way to allow duplicates as long as they are members of different documents?
Short answer: no, unique doesn't work like that.
When you tag a field unique in a mongoose schema, it will create a unique index on that field in MongoDB.
MongoDB index entries are made per-document, not per array entry.
For example, if you have an index on {field :1} and insert the document {field:["a","b","a","b","c","d"]}, then the entries in that index for this document will be:
"a"
"b"
"c"
"d"
In MongoDB, when an index is created with the unique: true option, it enforce that any value only appears once #in the index#. This means that the above document would be perfectly acceptable even if the index on {field: 1} were unique.
To quickly demonstrate this, I used your model defined above, and executed:
res1 = await (new MainModel({instances:[{name:"1"},{name:"2"},{name:"1"}]})).save();
console.log("Inserted: ",JSON.stringify(res1));
res2 = await MainModel.collection.getIndexes({full: true});
console.log("Indexes: ",JSON.stringify(res2));
res3 = await (new MainModel({instances:[{name:"3"},{name:"2"},{name:"4"}]})).save();
console.log("Inserted: ",JSON.stringify(res3));
This logged:
Inserted: {"_id":"60d273060ffb1ac8e48359e5","instances":[{"_id":"60d273060ffb1ac8e48359e6","name":"1"},{"_id":"60d273060ffb1ac8e48359e7","name":"2"},{"_id":"60d273060ffb1ac8e48359e8","name":"1"}],"__v":0}
Indexes: [{"v":2,"key":{"_id":1},"name":"_id_"},{"v":2,"unique":true,"key":{"instances.name":1},"name":"instances.name_1","background":true}]
/Users/joe/temp/mongoose/node_modules/mongodb/lib/core/error.js:57
return new MongoError(options);
^
MongoError: E11000 duplicate key error collection: test.mainmodels index: instances.name_1 dup key: { instances.name: "2" }
at Function.create
... snip ... {
driver: true,
index: 0,
code: 11000,
keyPattern: { 'instances.name': 1 },
keyValue: { 'instances.name': '2' }
}
As you can see, it create a unique index on { 'instances.name': 1 }, permitted duplicate entries within a single document, and prohibited an identical entry in the other document.

Mask data in Mongoose find operation

The requirement is to mask mobile number and show only last 4 digits. I do not want this to be performed at client instead mask it before sending the response. I am not sure how to modify transaction object to mask the data. I want to check if there is any mongoose function to do this. If not please suggest me the best way to mask a selected field.
Logic to fetch transactions
Transaction.find(query).populate('from','name mobile email').sort({ createdAt : -1 }).skip((page) * limit).limit(limit).exec((err, transaction) =>{
if(transaction){
Transaction.countDocuments({to:id,paymentStatus:"SUCCESS"},function(err,count){
return res.status(200).send({transaction,count:count});
});
}
else if(transaction==null) return res.status(200).send("No Transactions Found");
else if(err) return res.status(400).send("Error Occurred");
});
User.Model.ts - Merchant model is similar with some additional fields
var User= new mongoose.Schema({
email:{type:String,required:"E-Mail address cannot be empty",unique:true},
mobile:{type:String,required:"Mobile number cannot be empty",min : [10,"Please provide a valid 10 digit mobile number"],unique:true},
password:{type:String,required:"Password cannot be empty",minlength : [4,"Password must be more than 4 characters"]},
.......some unrelated fields...
});
Transaction.Model.ts
var transactionSchema = new mongoose.Schema({
from:{ type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
amount : {type:String,required:true},
to:{ type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Merchant' },
paymentStatus:{type : String, default : "INCOMPLETE"},
.......some unrelated fields...
});
Current output
{"transaction":[{"paymentStatus":"SUCCESS","isDisputed":true,"_id":"5eb8e50b3e2adb3b74e85d4f","from":{"_id":"5eb8e50a3e2adb3b74e85d43","name":"John Doe","email":"test#gmail.com","mobile":"9999999999"},"amount":"40","to":"5eb8e50a3e2adb3b74e85d46"}],"count":1}
Expected output
{"transaction":[{"paymentStatus":"SUCCESS","isDisputed":true,"_id":"5eb8e50b3e2adb3b74e85d4f","from":{"_id":"5eb8e50a3e2adb3b74e85d43","name":"John Doe","email":"test#gmail.com","mobile":"*******999"},"amount":"40","to":"5eb8e50a3e2adb3b74e85d46"}],"count":1}
You can use string-masking to mask the fields after you fetch them.
Mongoose plugin, virtuals or getters would also involve you to iterate over the array so the end result is same.
let stringMasking = require('string-masking');
...
transactions = transactions.map(transaction => {
let mask = stringMasking(transaction.from.phone, 0);
transaction.from.phone = mask.response;
return transaction;
});
...
return res.status(200).send({transaction,count:transaction.length});
Also its better to make the password not included in all find queries if not needed. Can be done by :
password: {type: String,select: false}

Mongoose create new data with incremented index-like field

What I want to do:
Whenever I add a new item to the collection (in my case a game), it will have a incremented "index"-like value (in my case I'm naming it index too).
My games collection should looks like:
[
{ "index":0, ... data }
{ "index":1, ... data }
{ "index":2, ... data }
]
The term is so hard to search. I always end up with:
$inc for update. Not this, I want to have incremented number on create.
Schema.index does look like what I want, but somehow it doesn't work at all:
const gameModel = new Schema({
index: {
type: Number,
default: 0
},
players: [{
name: String,
score: Number
}]
}, {
timestamps: {
createdAt: 'date'
}
});
gameModel.index({
index: 1
});
With this I always get index: 0. If I turn off default no index is created.
What do I do now? (I would prefer to keep the _id intact)
You can use a npm package named mongoose-auto-increment which provides this functionality. It is also very easy and well documented

String + autoincrement number on mongoose

I want to create a String + number every time a value is inserted on database (the number must be autoincrement).
Is it possible to do in Schema? Or do I need to do that before the value's inserted on database?
'question' -> String followed by an autoincrement number
var random = [{
question: 'question1'
},{
question: 'question2'
},{
question: 'question3'
},{
question: 'question4'
}];
const RandomSchema = new Schema({
question: {
type: String,
unique: true
}
})
Autoincrement fields in Mongodb, don't work exactly the same way that they do in an RDBMS. There is a lot more overhead involved. Never the less, creating an auto field is a solved problem. There is also a third party mongoose-autoincrement package that make it a lot easier.
Extending, from that. Your problem is a solved problem too. Because
the string part will always be the same
Simply use string concatenation to prepend 'question' to the auto increment field at display time.
Here is what I implemented with one of the approaches #e4c5 pointed out, without the use of a third-party package.
Define add.js as below:
db.counters.insert(
{
_id: "itemid",
seq: 0
}
)
function getNextSequence(id) {
var ret = db.counters.findAndModify(
{
query: { _id: id },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
db.randoms.insert(
{
question: "question"+getNextSequence("itemid"),
}
)
db.randoms.insert(
{
question: "question"+getNextSequence("itemid"),
}
)
After starting a mongod instance, do mongo < add.js.

Resources