Using TTL in MongoDB [duplicate] - mongodb

I have a very certain thing i want to accomplish, and I wanted to make sure it is not possible in mongoose/mongoDB before I go and code the whole thing myself.
I checked mongoose-ttl for nodejs and several forums and didn't find quite what I need.
here it is:
I have a schema with a date field createDate. Now i wish to place a TTL on that field, so far so good, i can do it like so (expiration in 5000 seconds):
createDate: {type: Date, default: Date.now, expires: 5000}
but I would like my users to be able to "up vote" documents they like so those documents will get a longer period of time to live, without changing the other documents in my collection.
So, Can i change a TTL of a SINGLE document somehow once a user tells me he likes that document using mongoose or other existing npm related modules?
thank you

It has been more than a year, but this may be useful for others, so here is my answer:
I was trying accomplish this same thing, in order to allow a grace period after an entry deletion, so the user can cancel the operation afterwards.
As stated by Mike Bennett, you can use a TTL index making documents expire at a specific clock time.
Yo have to create an index, setting the expireAfterSeconds to zero:
db.yourCollection.createIndex({ "expireAt": 1 }, { expireAfterSeconds: 0 });
This will not affect any of the documents in your collection, unless you set expireAfterSeconds on a particular document like so:
db.log_events.insert( {
"expireAt": new Date('July 22, 2013 14:00:00'),
"logEvent": 2,
"logMessage": "Success!"
} )
Example in mongoose
Model
var BeerSchema = new Schema({
name: {
type: String,
unique: true,
required: true
},
description: String,
alcohol: Number,
price: Number,
createdAt: { type: Date, default: Date.now }
expireAt: { type: Date, default: undefined } // you don't need to set this default, but I like it there for semantic clearness
});
BeerSchema.index({ "expireAt": 1 }, { expireAfterSeconds: 0 });
Deletion with grace period
Uses moment for date manipulation
exports.deleteBeer = function(id) {
var deferred = q.defer();
Beer.update(id, { expireAt: moment().add(10, 'seconds') }, function(err, data) {
if(err) {
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
};
Revert deletion
Uses moment for date manipulation
exports.undeleteBeer = function(id) {
var deferred = q.defer();
// Set expireAt to undefined
Beer.update(id, { $unset: { expireAt: 1 }}, function(err, data) {
if(err) {
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
};

You could use the expire at clock time feature in mongodb. You will have to update the expire time each time you want to extend the expiration of a document.
http://docs.mongodb.org/manual/tutorial/expire-data/#expire-documents-at-a-certain-clock-time

Related

how to sort by an auto-calculated field in mongoose.js

i have a product schema in my project, users can rate the products..the way i set the schema up is to store the number and sum of user ratings, then have a rating field where i would calculate the average of ratings so i can sort by it, but i can't find a way to automatically update this field whenever the product is updated.
the closest solution i had is to have the rating field's default value be a function that calculates the rating, but again it doesn't run automatically after the product's update
my schema looks like this
const productSchema = new Schema({
title: {
type: String,
required: true
},
views: {
type: Number,
default: 0
},
sumOfRatings: {
type: Number,
default: 0
},
numOfRatings: {
type: Number,
default: 0
},
rating: {
type: Number,
default: function() {
return this.sumOfRatings / this.numOfRatings
}
}, { collection: "Products", timestamps: true });
the question here describes something similar to what i want but there is no answer that does what i need
You can define a middleware for the findAndUpdateOne function, like this:
schema.pre('findOneAndUpdate', async function() {
const docToUpdate = await this.model.findOne(this.getQuery());
});
In this, you will first fetch the document and then set the updated value of the ratings.
i had to do something like this in the end..find the place, get sum and num of ratings from it and then calculate rating and do another query to update it with it, then call the updating function whenever needed..not a big fan of this solution but that's what i could come up with
function updateProductRating(id) {
Product.findById(id).then(product=> {
let calcRating = product.sumOfRatings / product.numOfRatings;
Product.findByIdAndUpdate(id, { rating: calcRating }).then(result=>{
console.log("###### update rating ######")
console.log(result)
})
})
}

Trying to design Facebook-like notification in mongoose. Need help aggregating

Like Facebook, I would like to aggregate the results. But I can't figure out how to go about it.
Example:
Let's say 10 users like my posts.
I don't want to get 10 notifications. 1 is of course enough.
This is my schema:
var eventLogSchema = mongoose.Schema({
//i.e. Somebody commented, sombody liked, etc.
event: String,
//to a comment, to a post, to a profile, etc.
toWhat: String,
//who is the user we need to notify?
toWho: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
//post id, comment id, whatever..
refID: {type: mongoose.Schema.Types.ObjectId},
//who initiated the event.
whoDid: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
// when the event happened
date: {type:Date, default: Date.now()},
//whether the user already saw this notification or not.
seen: {type:Boolean, default: false}
})
so I need to count the times
Ex.1: event='liked' and toWhat="post" and refID=myPostID and seen=false
But at the same time, I would like to populate the last event with this parameters on the 'who' path so I could display "Michael and 9 other people liked your post(link to post)"
Every way I can think of doing this is clunky and requires multiple queries that feel like they would cost a lot of system resources and I am wondering if there's a simple way to do it.
Actually it gets more complicated then that.
I do not want to specify values like I did in Ex.1.
Instead I would like to say
aggregate all events with similar 'event', 'toWhat',
'refID' with value seen=false and populate the last one on the 'who' path.
Would love some reading materials, links, advice, or anything.
Thanks!
Managed to solve it like this.
Not sure if it's optimal, but it works.
//The name of my Schema
Notification.aggregate([
{
$match: {
//I only want to display new notifications
seen: {$ne: true}
//to query a specific user add
// toWho: UserID
}
},
{
$group: {
//groups when event, toWhat, and refID are similar
_id: {
event: '$event',
toWhat: '$toWhat',
refID: '$refID',
},
//gets the total number of this type of notification
howMany: {$sum: 1},
//gets the date of the last document in this query
date: {$max: '$date'},
//pulls the user ID of the last user in this query
user: {$last: '$whoDid'}
}
}
]).exec(function (err, results) {
if (err) throw err;
if (results) {
//after I get the results, I want to populate my user to get his name.
Notification.populate(results, {path: 'user', model: "User"}, function (err, notifications) {
if (err) throw err;
if (notifications) res.send(notifications);
})
}
})
I'm not sure whether it's possible to populate the aggregated result in one query, I assume that if it's possible it would be optimal, but so far, this seems acceptable for my needs.
Hope this helps.

Meteor upsert with mongo

I need to use upsert because something a record in my Accounts schema (see below) may not exist. The defining factors for its existence are adviserID and period. If a record that matches both of those exists it should just update the record.
However when I run this, I get this error: Sanitized and reported to the client as: Adviser id must be an object [400]. When I console.log(adviser) inside the updateOrder method it does indeed report correctly the adviser ObjectID.
Any help would be appreciated. Thank you.
Upsert method:
Meteor.methods({
updateOrder: function(adviser, order, period) {
Account.upsert(
{
adviserId: adviser,
period: period
}, {
$set: {
adviserId: adviser,
order: order,
outstanding: order,
period: period
}
});
}
});
Schema:
Schema.Account = new SimpleSchema({
"adviserId": {
type: Object,
},
period: {
type: Date,
label: "Order Period"
},
order: {
type: Number,
min: 0,
label: "Order Number"
},
outstanding: {
type: Number,
min: 0,
defaultValue: 0,
label: "Outstanding Number"
}
});
From SimpleSchema Docs:
If you have a key with type Object, the properties of the object will be validated as well, so you must define all allowed properties in the schema.
adviser in your method should be a string because you use it for the find (in upsert). If your adviser is a string, then just fix your schema for {type: String, regEx: SimpleSchema.RegEx.Id}. Or if you want to use an object, then you need describe it or add blackbox option.
More than likely you want adviserId to be a string. MongoDB internal ID objects are strings in Meteor.
This is how we upsert in meteor.
Shares.update({
adviserId: adviser,
period: period
}, {
$set: {
adviserId: adviser,
order: order,
outstanding: order,
period: period
}
}, {
upsert: true
})
Thank you Bryukhanov Valentin. Converting to type String resolved my issues.
Thanks also to those that replied.

Auto increment document number in Mongo / Mongoose

My app has several users, each user has documents. Each documents needs to have a sequence number, that may look something like this: 2013-1, 2013-2 (year and sequence number), or perhaps just a simple number: 1, 2, 3...
Currently, I am assigning the sequence number from user's settings when the Mongoose docuemnt is created. Based on that sequence number and the number format from user's settings, I am generating the final document number.
What I realized is that when 2 documents are created at the same time, they will get exactly the same number, because I am incrementing the sequence number in settings just after I have saved a document. But I am assigning the sequence number when I am creating (not saving yet) the document so the sequence number will be exactly the same for both documents.
I obviously need a way to handle this sequence number auto-incrementing at the moment of saving...
How can I assure that this number is unique and automatically incremented/generated?
#emre and #WiredPraire pointed me to the right direction, but I wanted to provide a full Mongoose-compatible answer to my question. I ended up with the following solution:
var Settings = new Schema({
nextSeqNumber: { type: Number, default: 1 }
});
var Document = new Schema({
_userId: { type: Schema.Types.ObjectId, ref: "User" },
number: { type: String }
});
// Create a compound unique index over _userId and document number
Document.index({ "_userId": 1, "number": 1 }, { unique: true });
// I make sure this is the last pre-save middleware (just in case)
Document.pre('save', function(next) {
var doc = this;
// You have to know the settings_id, for me, I store it in memory: app.current.settings.id
Settings.findByIdAndUpdate( settings_id, { $inc: { nextSeqNumber: 1 } }, function (err, settings) {
if (err) next(err);
doc.number = settings.nextSeqNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
next();
});
});
Please note that with this method there is no way to require the number path in the schema, and there is no point as well, because it is automatically added.
You can achieve that through:
create sequence generator, which is just another document that keeps a counter of the last number.
Use a mongoose middleware to update the auto increment the desired field.
Here is a working and tested example with the todo app.
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/todoApp');
// Create a sequence
function sequenceGenerator(name){
var SequenceSchema, Sequence;
SequenceSchema = new mongoose.Schema({
nextSeqNumber: { type: Number, default: 1 }
});
Sequence = mongoose.model(name + 'Seq', SequenceSchema);
return {
next: function(callback){
Sequence.find(function(err, data){
if(err){ throw(err); }
if(data.length < 1){
// create if doesn't exist create and return first
Sequence.create({}, function(err, seq){
if(err) { throw(err); }
callback(seq.nextSeqNumber);
});
} else {
// update sequence and return next
Sequence.findByIdAndUpdate(data[0]._id, { $inc: { nextSeqNumber: 1 } }, function(err, seq){
if(err) { throw(err); }
callback(seq.nextSeqNumber);
});
}
});
}
};
}
// sequence instance
var sequence = sequenceGenerator('todo');
var TodoSchema = new mongoose.Schema({
name: String,
completed: Boolean,
priority: Number,
note: { type: String, default: '' },
updated_at: { type: Date, default: Date.now }
});
TodoSchema.pre('save', function(next){
var doc = this;
// get the next sequence
sequence.next(function(nextSeq){
doc.priority = nextSeq;
next();
});
});
var Todo = mongoose.model('Todo', TodoSchema);
You can test it out in the node console as follows
function cb(err, data){ console.log(err, data); }
Todo.create({name: 'hola'}, cb);
Todo.find(cb);
With every newly created object the you will see the priority increasing. Cheers!
This code is taken from MongoDB manual and it actually describes making the _id field auto increment. However, it can be applied to any field. What you want is to check whether the inserted value exists in database just after you inserted your document. If it is allready inserted, re increment the value then try to insert again. This way you can detect dublicate values and re-increment them.
while (1) {
var cursor = targetCollection.find( {}, { f: 1 } ).sort( { f: -1 } ).limit(1);
var seq = cursor.hasNext() ? cursor.next().f + 1 : 1;
doc.f = seq;
targetCollection.insert(doc);
var err = db.getLastErrorObj();
if( err && err.code ) {
if( err.code == 11000 /* dup key */ )
continue;
else
print( "unexpected error inserting data: " + tojson( err ) );
}
break;
}
In this example f is the field in your document that you want to auto increment. To make this work you need to make your field UNIQUE which can be done with indexes.
db.myCollection.ensureIndex( { "f": 1 }, { unique: true } )
You can use mongoose-auto-increment package as follows:
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
/* connect to your database here */
/* define your DocumentSchema here */
autoIncrement.initialize(mongoose.connection);
DocumentSchema.plugin(autoIncrement.plugin, 'Document');
var Document = mongoose.model('Document', DocumentSchema);
You only need to initialize the autoIncrement once.

Mongoose: How to model a foreign key/inverse relationship?

I am using Mongoose to model Person and Transaction collections, where each Transaction will have references to two different Person instances:
var TransactionSchema = new Schema({
, amount : { type: Number, required: true }
, from : { type: ObjectId, required: true }
, to : { type: ObjectId, required: true }
, date : Date
});
var PersonSchema = new Schema({
name : { type: String, required: true }
, transactions : [ObjectId]
});
I'd like each Person to have a collection of all the Transactions that they are either the to or from value for. So far, this is the best way I've been able to figure out how to do it:
TransactionSchema.pre('save', function(next, done) {
var transaction = this;
Person.findById(this.to, function (err, person) {
person.transactions.push(transaction);
person.save();
});
Person.findById(this.from, function (err, person) {
person.transactions.push(transaction);
person.save();
});
next();
});
This seems excessive. Is there a better way to do it, or am I trying to use MongoDB too much like a relational database? Instead of having a collection of Transactions associated with each Person instance, should I just be querying the Translation collection directly?
Thank you.
You've got to think more on the queries you are going to execute on the database when you design the MongoDB schema.
Try to duplicate data for speed and reference it for integrity. What does that mean?
Well, for example when you make a query for a Transaction, I guess you don't need all the user details from the first time no? (do you need the user's email, location when displaying info on a Transaction?)
I think you just probably need the user id and the username, so you should do something like this:
var TransactionSchema = new Schema({
, amount : { type: Number, required: true }
, from : {
user_id: {
type: ObjectId
, required: true
}
, username: {
type: String
, required: true
}
}
, to : {
user_id: {
type: ObjectId
, required: true
}
, username: {
type: String
, required: true
}
}
, date : Date
});
So instead of doing 3 queries for the page displaying the Transaction details (one for the transaction and 2 additional queries for the usernames), you'll have just one.
This is just an example, you could apply the same logic for the User schema, depending on what you're trying to achieve.
Anyway I don't think your middleware is ok, since you are not checking for errors there (you are always calling next no matter what). This is how I would write the middleware (didn't test, but the idea is important):
TransactionSchema.pre('save', function(next, done) {
var transaction = this;
Person.where('_id').in([this.to, this.from]).run(function (err, people) {
if (people.length != 2) { next(new Error("To or from doesn't exist")); return; }
Step(
function save_to() {
people[0].transactions.push(transaction);
people[0].save(this);
},
function save_from(err) {
if (err) { next(err); return; }
people[1].transactions.push(transaction);
people[1].save(this);
},
function callback(err) {
next(err);
}
);
});
});
In the code above I'm using the Step library for flow control and I'm only using one query instead of two (when searching for "to" and "from").