How do I create a compound unique autoincrementing field in mongoose? - node.js

I would like to create a schema where I can set a specific name for a user, for example a display name that many people can have, while also allowing people to add them based on that name. My solution is to save that name into a model with two fields, one of which is the name itself, the second one having an id that represents the number of times that name is being used currently.
var DisplayName = new Schema({
account: {
type: ObjectId,
ref: 'Account',
},
name: {
type: String,
trim: true,
required: true,
},
number: {
type: Number,
required: true,
}
});
is what I have, but I would like to be able to just say
var displayName = new DisplayName({
name: 'name',
account: req.user.id
});
displayName.save();
and it would automatically set the field for me?
EDIT: I realize there wasn't enough detail in the original question:
I'd like to use mongoose's built-in post-save handlers to generate this field for me on creation. It shouldn't be updated after its created, only set the first time.

You can use a function as shown below to do that thing.
function saveDisplayName(schema) {
var query = DisplayName.find({name: schema.name});
query.then(function(results) {
var userNumber = 1;
if (results.length < 1) {
return userNumber;
}
for (var i = 0; i <= results.length - 1; i++) {
var result = results[i];
if (userNumber <= result.number) {
userNumber = result.number + 1;
}
if (i == results.length - 1) {
return userNumber;
}
}
}).then(function(userNumber) {
schema.number = userNumber;
var displayName = new DisplayName(schema);
return displayName.save();
}).then(function(data) {
console.log('Data saved', data)
}).then(null, console.log);
}

Related

(Mongoose) Failed to remove a TTL field

I have a authentication system where the user need to validate their email after creating an account, then the status of the account would change from Pending to Active.
I want to implement a feature that if a user does not validate their email within a time frame, the account will be deleted. I was able to get the delete part done by setting an expire field in the Schema:
const userSchema = new mongoose.Schema<IUser>(
{
email: {
type: String,
required: true,
},
status: {
type: String,
default: "Pending",
required: true,
},
expiresAt: {
type: Date,
default: () => Date.now() + 30 * 1000,
expires: 30
}
},
{
timestamps: true
}
However, the expire counter seem to be still counting even I purposely deleted the expiresAt field, and the document still get deleted.
const user = await User.findById(tokenId).exec();
user.status = 'Active';
user.expiresAt = undefined;
const updatedUser = await user.save();
I wonder is there a way to stop or remove the TTL feature entirely after validation?
This is actually a feature provide by the mongoose schema, You are not actually deleting the expiresAt value, the mongoose schema ignored that input as it does not match the type.
By looking at mongoose source code we can see this:
doc: { status: "Active", expiredAt: undefined }
const keys = Object.keys(obj);
let i = keys.length;
while (i--) {
key = keys[i];
val = obj[key];
if (obj[key] === void 0) {
delete obj[key];
continue;
}
}
The expressions obj[key] === void 0 means if obj[key] === undefined like in your case, basically mongoose removes this value from the update body.
What you want to do is just use an operator like $unset:
db.collection.updateOne({ _id: user._id}, {$set: {status: "Active"}, $unset: {expiresAt: ""}})

How do I create a number of objects with insertMany with existing data from the db?

I'm currently trying to insert a large number of models through insertMany, but I can't seem to figure out how to populate the array when creating an object. I'm relatively new to Mongoose and any help would be appreciated, here is the code I have right now.
const ProgramsSchema = new mongoose.Schema({
program_id: {
type: String,
required: true
},
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: {
type: [{type: ProgramsSchema, ref: "Programs"}]
}
});
And here's the code where I try to create a number of schools and add it to the database.
let new_schools = []
for (let i = 0; i < schools.length; i++) {
let school = schools[i]
let p_arr = []
for (let p_index = 0; p_index < school["PROGRAMS"].length; p_index++) {
let p_id = school["PROGRAMS"][p_index]
Programs.find({program_id: p_id}).populate('Programs').exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0])
}
})
}
let newSchool = {
inst_url: school["INSTURL"],
programs: p_arr,
}
new_schools.push(newSchool);
}
Schools.insertMany(new_schools);
I can basically add all of the school data into the db, but none of the programs are being populated. I was wondering if there was a way to do this and what the best practice was. Please let me know if you guys need more info or if my question wasn't clear.
There are a few problems with your mongoose schemas. The operation you are trying to do in find is not available, based on your mongoose schemas. You cannot populate from "Programs" to "Schools". You can populate from "Schools" to "Programs", for instance:
Schools.find().populate(programs)
And to do that, several changes in your schemas are necessary. The idea is to store the programs _id in your programs array in School collection and be able to get the programs info through populate(), either regular populate or 'custom populate' (populate virtuals).
Regular populate()
I would change the schoolsSchema in order to store an array of _id into programs:
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
You should change ProgramsSchema as well:
const ProgramsSchema = new mongoose.Schema({
_id: Schema.Types.ObjectId, // that's important
description: {
type: String
},
});
And now, you can do:
Programs.find({_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0]._id)
}
})
Your documents should be inserted correctly. And now you can populate programs when you are performing a query over School, as I indicated above:
Schools.find().populate(programs)
Populate Virtual
The another way. First of all, I have never tried this way, but I think it works as follows:
If you want to populate over fields that are not ObjectId, you can use populate virtuals (https://mongoosejs.com/docs/populate.html#populate-virtuals). In that case, your schemas should be:
const ProgramsSchema = new mongoose.Schema({
program_id: String,
description: {
type: String
},
});
const schoolsSchema = new mongoose.Schema({
inst_url: {
type: String
},
programs: [
{type: String, ref: "Programs"}
]
});
Enable virtual in your School schema:
Schools.virtual('programs', {
ref: 'Programs',
localField: 'programs',
foreignField: 'program_id'
});
Then, you should store the program_id.
Programs.find({program_id: p_id}).exec(function(err, data) {
if (err) {
console.log(err);
} else {
p_arr.push(data[0].program_id)
}
})
And as before, you can populate() when you need.
I hope I helped

Get Parent Schema Property for Validation with Mongoose and MongoDB

Let's say that I have a nested Schema (invitation) in another schema (item) and when trying to save an invitation object, I want to check if the parent property 'enabled' from the item schema is set to true or false before allowing the person to save the invite object to the invitations array. Obviously, this.enabled doesn't work as it's trying to get it off of the invitationSchema, which doesn't exist. How would one get the 'enabled' property on the parent schema to validate?
Any thoughts? Thanks for any help!
var validateType = function(v) {
if (v === 'whateverCheck') {
return this.enabled || false; <== this doesn't work
} else {
return true;
}
};
var invitationSchema = new Schema({
name: { type: String },
type: {
type: String,
validate: [validateType, 'error message.']
}
});
var itemSchema = new Schema({
title: { type: String },
description: { type: String },
enabled: {type: Boolean}, <== trying to access this here
invitations: { type: [ invitationSchema ] },
});
var ItemModel = mongoose.model('Item', itemSchema, 'items');
var InvitationModel = mongoose.model('Invitation', invitationSchema);
The parent of an embedded document is accessible from an embedded doc model instance by calling instance.parent();. So you can do this from any Mongoose middleware like a validator or a hook.
In your case you can do :
var validateType = function(v) {
if (v === 'whateverCheck') {
return this.parent().enabled || false; // <== this does work
} else {
return true;
}
};

Mongoose field match populate

I'm having some trouble with how best to implement this join/query in mongoose.
var mongoose = require('mongoose');
var addressSchema = new mongoose.Schema{
rank: Number,
address: String,
tag_name: String,
tag_url: String};
var tagSchema = new mongoose.Schema{
address: String,
name: String,
url: String};
I have a bunch of addresses saved and a bunch of tags saved. Some addresses have tags, most do not. I update the addresses and tags separately frequently. What I want to do is query some specific addresses and return them as an array with the tag fields filled in (the address tag fields are blank in the database).
So for example, I want to do this without making a db query for every address(101 db queries in the example). I'm not sure if $match or $in or populate is what I'm looking for. The below code is untested and may not work, but it should give you an idea of what I'm trying to do.
var db = require('../config/dbschema');
// find an array of addresses
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
// now fill in the tag fields and print
addTagField(0, addresses);
});
// recursive function to fill in tag fields
// address tag name and tag url fields are blank in the database
function addTagField(n, addresses) {
if(n < addresses.length) {
db.tagModel.find( { address: addresses[n].address }, function(err, tag) {
// if find a tag with this address, fill in the fields
// otherwise leave blank and check next address in array
if(tag) {
addresses[n].tag_name = tag.name;
addresses[n].tag_url = tag.url;
}
addTagField(n+1, addresses);
});
} else {
console.log(addresses);
}
}
http://mongoosejs.com/docs/api.html#aggregate_Aggregate-match
http://mongoosejs.com/docs/api.html#query_Query-in
http://mongoosejs.com/docs/api.html#document_Document-populate
I want to do what the above does with fewer db queries.
Your major problem is that you're not taking advantage of Mongoose's relationship mapping. Change your schemas just a bit and your problem will easily be solved. You can do it like this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [tagSchema],
})
addressModel.find({rank: {$lte: 100}}, function(err, addresses) {
...
})
or this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [{type: ObjectId, ref: 'Tag'}],
})
addressModel
.find({rank: {$lte: 100}})
.populate('tags', 'name url')
.exec(function(err, addresses) {
...
})
I didn't want to embed the docs. The is what I came up with.
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
if(err) return res.send(400);
if(!addresses) return res.send(404);
var addrOnlyAry = addresses.map(function(val, idx) { return val.address; });
db.tagModel.find( { address: { $in: addrOnlyAry } }, {}, function(err, tags) {
if(err) return res.send(400);
if(tags.length > 0) addresses = setTagFields(addresses, tags);
return res.json(addresses);
}
}
function setTagFields(addresses, tags) {
for(var i=0; i < tags.length; i++) {
for(var j=0; j < addresses.length; j++) {
if(addresses[j].address === tags[i].address) {
addresses[j].tag_name = tags[i].tag;
addresses[j].tag_url = tags[i].url;
break;
}
}
}
return addresses;
}

Sorting by length of array, if it's not filled in the model

So, i have two mongoose models:
User: {
name: String,
..........
locations: []
}
Location: {
title: String,
.............
owner: {
type: Schema.Types.ObjectId,
ref: 'User'
}
}
my lookup function in node, for User is something like:
api.find = function(filter, sort, skip, limit){
var callback = arguments[arguments.length - 1];
filter = treatFilter(filter);
sort = treatSort(sort);
if(callback && typeof callback === 'function'){
User.find(filter).sort(sort).skip(skip).limit(limit).exec(function(err, users){
if(users.length === 0){
callback(err, users);
}
for(var i = 0, complete = 0; i < users.length; i++){
(function(idx){
var user = users[idx];
var id = user.id;
Location.find({owner: id}, function(err, locations){
user.locations = locations;
if(++complete === users.length){
callback(err, users);
}
});
})(i);
}
});
}
};
what would my sort object have to be like so i could sort by number of locations?
Thanks, if you have any doubts i will be happy to explain my code
According to this post (mongoose - sort by array length)
You have to maintain a separate field that stores the location count -- not too much overhead but a little annoying to maintain
I looked into sorting by a virtual field, but this too is impossible and probably would be quite inefficient...

Resources