How to set default value as a populated 3D array and still define the objects within it? - mongodb

I want to make a 3d array that will represent years, months and days:
[years[months[days]]]
And so I made a simple function that does this perfectly, for each day I am populating a default object:
function generateEmpty3dArray() {
// i did that on purpuse with [[]], im doing it just for 1 year atm,
// thats why i am using index 0
const array = [[]];
const months = 12;
for(let month = 0; month < months; month++) {
const monthDays = new Date(2021, month + 1, 0).getDate();
array[0].push([]);
for(let day = 0; day < monthDays; day++) {
array[0][month].push({shift: "X"})
}
}
console.log(array)
}
The array that is generated is exactly what I want, my issue is in defining the Person schema:
const personSchema = new mongoose.Schema({
schedule: {
type: Array,
default: generateEmpty3dArray() //????
}
})
I dont know how to populate the default field, what I want to happen is, whenever a new Person is created into the database, I want him to receive that empty 3d array, so that I can then work with it and set different shifts.
The problem is that, I have no idea how to both specify what the specific value of each day should look like [[[{shift: {type: String, enum: shifts}}]]] and at the same time, create the default array for the whole thing.

This is what worked:
schedule: {
type: [
[
[
{
shift: { type: String, required: true, enum: shifts },
},
],
],
],
default: generateEmpty3dArray(),
},

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)
})
})
}

mongodb need to populate a new field with an old fields value, without destroying other data

I have a situation where a model changed at some point in time and I am faced with (for argument sake) half my data liks like this
{
_id: OID,
things: [{
_id:OID,
arm: string,
body: string
}],
other: string
}
and the other half of my data look like this
{
_id: OID,
things: [{
_id:OID,
upper_appendage: string,
body: string
}],
other: string
}
I would like to 'correct' half of the data - so that I DON'T have to accommodate both names for 'arm' in my application code.
I have tried a couple different things:
The first errors
db.getCollection('x')
.find({things:{$exists:true}})
.forEach(function (record) {
record.things.arm = record.things.upper_appendage;
db.users.save(record);
});
and this - which destroys all the other data in
db.getCollection('x')
.find({things:{$exists:true}})
.forEach(function (record) {
record.things = {
upper_appendage.arm = record.things.upper_appendage
};
db.users.save(record);
});
Keeping in mind that there is other data I want to maintain...
How can I do this???
the $rename operator should have worked for this job but unfortunately it doesn't seem to support nested array fields (as of mongodb server 4.2). instead you'd need a forEach like the following:
db.items.find({
things: {
$elemMatch: {
arm: {
$exists: true
}
}
}
}).forEach(function(item) {
for (i = 0; i != item.things.length; ++i)
{
item.things[i].upper_appendage = item.things[i].arm;
delete item.things[i].arm; ;
}
db.items.update({
_id: item._id
}, item);
})
note: i've assumed you want to make all records have upper_appendageand get rid of 'arm' field. if it's the other way you want, just switch things around.

Buffer grouped observables

I have the following class and array
class Hero {
id: number;
name: string;
}
const HEROES: Hero[] = [
{ id: 11, name: 'Narco' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Bombasto' },
{ id: 15, name: 'Bombasto' },
{ id: 16, name: 'Dynama' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dynama' },
{ id: 19, name: 'Dynama' },
{ id: 20, name: 'Dynama' }
];
I want to create an observable that treats the HEROES array as the source, groups the array by name and emits the result as a single array, i.e., I should end up with three arrays, one for Narco, one for Bombasto and one for Dynama.
I can create my source observable as follows
var heroSource = Observable.create((observer:any) => {
HEROES.forEach((hero: Hero) => {
observer.next(hero);
})
});
I can then group heros by using groupBy, i.e.,
var groupHeroSource = heroSource
.groupBy((hero: Hero) => {return hero.name});
This effectively gives me as many observables as there are different names in my HEROES array (three in this case). However, these will be emitted as a sequence and ideally I'd like to buffer them until heroSource is complete. How would I use the buffer operator in rxjs on my grouped observables so that emit a single collection?
First of all you can create your initial observable much simpler:
var heroSource = Observable.from(HEROES);
Next, your groupBy mapper/selector can be abbreviated to:
var groupHeroSource = heroSource.groupBy((hero: Hero): string => hero.name);
To solve the original problem I needed to buffer the streams, since they are ready a buffer with time of 0 would do the work (I guess there should be a more elegant solution out there), use take(1) to take only the first result (and avoid a repeating buffer) and then merge all:
var finalGroup = groupHeroSource.map((ob) => ob.bufferWithTime(0).take(1)).mergeAll();
Note that since that since your array is actually static, putting it through a stream and then mapping it might not be the simplest solution, you can simply reduce it:
var grouped = HEROES.reduce((acc: any, hero: Hero) => {
acc[hero.name] = acc[hero.name] || [];
acc[hero.name].push(hero);
return acc;
}, {});
Since Object.values is not standard, you'll have to iterate the keys to get an array, yet, it might be a better fit for your need

Adding an item to a cursor MeteorJS

Here is my code right now:
Template.home.helpers({
categories: function(){
// Categories is a collection defined earlier
return Categories.find();
},
});
var categories = Categories.find();
/**
categories.append({
name: "All",
icon: "home"
});
*/
return categories;
},
It is just returning all the categories from the database I am using. I want to make an aggregate category. For example, saw I have 2 categories:
[
{
name: "Link",
views: 5
},
{
name: "Time",
views: 10,
}]
Say I want a 3rd category:
{
name: "All",
views: 15 // this is from 10 + 5
}
How would I go about adding that to the cursor?
Instead of returning a cursor, a helper can also return an array (or a single value). Therefore, you can solve your problem by fetching all of the existing categories into an array, modifying it with the desired data, and returning the modified array. Here is an example implementation:
Template.home.helpers({
categories: function() {
// fetch all of the categories into an array
var cats = Categories.find().fetch();
// compute the total views for all categories
var totalViews = _.reduce(cats, function(memo, cat) {
return memo + cat.views;
}, 0);
// add a new 'category' with the total views
cats.push({name: 'All', views: totalViews});
// return the array of modified categories
return cats;
}
});

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.