Array object is not iterable - javascript

I have this model in Mongoose and NodeJS:
const VisitorSchema = new Schema({
visits: [{date: Date, path: String, details: String}],
// ...
});
And this code:
// Get all visitors.
VisitorSchema.statics.getAllVisitors = function() {
try {
// Return all users.
return Visitor.find()
.sort({created: -1})
.exec();
} catch (err) {
console.log(err);
}
};
// Check and print new users.
VisitorSchema.statics.checkPaths = async function() {
console.log("Checking paths...");
let visitors = await this.getAllVisitors();
for (let visitor of visitors) {
try {
for (let v of visitors.visits) {
// ...
}
} catch (e) {
console.log(e);
console.log(Object.prototype.toString.call(visitor.visits));
throw(e);
}
}
};
Running this function unexpectedly throws:
Checking paths...
TypeError: visitors.visits is not iterable
at Function.VisitorSchema.statics.checkPaths
[object Array]
5efba3a0a97a823909802df5
(node:23663) UnhandledPromiseRejectionWarning: TypeError: visitors.visits is not iterable
at Function.VisitorSchema.statics.checkPaths
at processTicksAndRejections
....
I also checked the MongoDB object in the mongo shell and the sub-document visits for the relevant document is an array and seems OK:
> db.visitors.findOne({_id: ObjectId("...")})
{
"_id" : ObjectId("..."),
"visits" : [
{
"_id" : ObjectId("..."),
"date" : ISODate("..."),
"path" : "/"
},
...
]
}
How can an Array object not be iterable?

An array is always iterable in JS. Pay attention to the rows in the visitors collection, which may lack the visits property. If that property is not an array in MongoDB (this is allowed because MongoDB is a NoSQL database) it will still be cast to an empty array from your model definition.
In your specific case, you have a typo:
for (let v of visitors.visits) // plural
should probably be
for (let v of visitor.visits) // singular

Related

how to save objects from a JSON in different documents with mongoose?

Well I have the following doubt. I have the following:
JSON
[
{"name": "juan", "age": 10}
{"name": "pedro", "age": 15}
{"name": "diego", "age": 9}
]
User Schema
_group:{
type: mongoose.Schema.Types.ObjectId,
ref: 'Group'
},
name: {
type: String
},
age: {
type: Number
}
And I need to save or update this data in different docs with nodejs/mongoose. I planned to do the following
var data = JSON.parse(json)
for (var i = data.length - 1; i >= 0; i--) {
var name = data[i].name;
var age = data[i].age;
User.find({'name': par, '_group': group_id}, (err, user)=>{
if(err)
next(err);
// if it does not exist, create new doc
if(_.isEmpty(doc)){
var newuser = new User;
newuser.name = name;
newuser.age = age;
newuser.save((err, saved)=>{
})
}// if it exists, update it
else if(!_.isEmpty(doc)){
user.age = age;
user.save((err, saved)=>{
})
}
})
}
as you will see, the variables age and name within User.find remain undefined, so this does not work for me.
First of all, is it the right way to save this data? If so, how could I can use the for cycle variables (name and age) within User.find? If not, what do you recommend me to do?
Thanks,
Eduardo
NodeJS, ExpressJS, Mongoose
There is one more issue which I think you are facing that you are calling a method inside a loop and it takes a call-back, so it doesn't wait here for coming back and move to second iteration, so you might face undefined and some un-expected behavior.
I suggest you should use async/await
let user = await User.findOneAndUpdate({'name': par, '_group': group_id}, { name, age }, { upsert: true })
If you parsed given JSON well and assigned values to name and age, they are not undefined within User.find scope.
Did you checked those variables?
var name = data[i].name;
var age = data[i].age;
You can use mongoose findOneAndUpdate with the option { upsert: true }.
This tries to update an object in the DB and, if the object is not found, it creates it. So:
for (var i = data.length - 1; i >= 0; i--) {
var name = data[i].name;
var age = data[i].age;
User.findOneAndUpdate({'name': par, '_group': group_id}, { name, age }, { upsert: true, new: true, lean: true }, (err, updated) => {
if(err) console.log(err);
else console.log(updated);
})
}
The option new tells to return the updated object and the option lean tells to return a plain JSON, instead of Mongoose document object (the same as calling doc.toJson())
Using the upsert option, you can use findOneAndUpdate() as a find-and-upsert operation. An upsert behaves like a normal findOneAndUpdate() if it finds a document that matches filter. But, if no document matches filter, MongoDB will insert one by combining filter and update as shown below.
data.forEach( user => {
User.findOneAndUpdate({'name': user.name , '_group': group_id}, user , {upsert: true, new: true}, (err, data) => {
if(err) console.log(err);
console.log(data);
})
})

Object.assign in map returns unexpected response with extra information

I'm trying to systematically replace the key value in a JSON object with a new value that I calculate:
newReport = filteredReport
sumFunc = (items, prop) => {
return items.reduce( function(a, b){
return a + b[prop];
}, 0);
}
complete = newReport.map((v,i) => {
let newValue = sumFunc(newReport[i].like, 'value')
//return { ...v, like: newValue }
return Object.assign({}, v, {like: newValue})
})
filteredReport, i.e., the original value, is a result of a query from MongoDB.
When I run this function, I get a response that's 340,000 lines long. The correct result is in place in the last line, but I get a lot of other undesired information in there that is unexpected.
Why do I receive this information?
This is a sample of the response: https://codepen.io/schoenbl/pen/pXVRBq?editors=0010#0
EDIT:
When I use spread and Object.assign, I receive the same result.
Another Edit:
When I preview it outside, it works correctly:
https://playcode.io/360520?tabs=script.js,preview,console
Last Edit:
This is my mongo query:
const report = await Report.find({createdAt: { $gt: startDate, $lt: Date() }})
.populate({path: 'like'})
.populate({
path: 'player',
populate: [{ path: 'team' },
{
path: 'team',
populate: {
path: 'league'
}
}
]
})
When you query it returns an array Mongoose document instance. But when you console log it shows you the toString representation of it. But actually, it contains all those functions properties of the Document class instance to do further operations. To return only data use .lean() when querying.
Report.find().lean().exec(function (err, report) {
....
}
This will return only the plain js object of the data that you want. Not all the class properties and methods.
Tip: To know what a variable really contains, use console.dir instead of console.log

Meteor Collection.insert() not returning Id

I'm trying to get the Id of the new insert so I can push the Id onto another collection.
According to this post =>
Meteor collection.insert callback to return new id and this post => Meteor collection.insert callback issues, I should be able to
return Collection.insert(obj);
and it will return the ID of the newly inserted data to my client.
Instead I'm getting an Observable like this:
{_isScalar: false}
_isScalar: false
__proto__:
constructor: f Object()
hasOwnProperty: f hasOwnProperty()
//many other object properties
The documentation seems pretty clear that I should be getting the ID in return. Is this a bug? https://docs.meteor.com/api/collections.html#Mongo-Collection-insert]
My version of meteor is 1.4.4.5...
I've been working on this issue for a few days and I've tried getting the ID many different ways, nothing I've tried results in the ID.
Here's my full code for reference:
Server:
submitStuff: function(data): string{
var loggedInUser = Meteor.user();
if (!loggedInUser || !Roles.userIsInRole(loggedInUser,['user','admin'])){
throw new Meteor.Error("M504", "Access denied");
} else {
try{
let insertedData = null;
const model: MyModel[] = [{
data.stuff //bunch of data being inserted
}];
model.forEach((obj: MyModel) => {
insertedData = Collection.insert(obj);
});
return insertedData;
} catch(e) {
throw new Meteor.Error(e + e.reason, "|Throw new error|");
}
}
},
Client:
Meteor.call('submitData', data, (error, value) => {
if(error){
this.errors.push(error + error.reason + "|new Error code|");
}
else{
Meteor.call('userPushId', value, (error) => { //this pushes Id onto the second collection
if(error){
this.errors.push(error + error.reason + "|new Error code|");
}
});
}
Server
// ...
try {
const myModels = [{ ...whatever }];
// I'm using map so I return an array of id's.
//your forEach about technically should end up with only one id,
// which is the last insertion
const insertedDataIds = myModels.map(model => Collection.insert(model));
// you can also write to the secondary location here with something like:
const secondaryWrite = SecondaryCollection.insert({ something: insertedDataIds });
return insertedDataId's
}
//...
Client
also I don't know if this is just a typo on stack but your Meteor.call('submitData') should be Meteor.call('submitStuff') but that is probably not your actual issue.
Alright, so after searching around I realized I'm using angular-meteors rxjs package to create a MongoObservable when creating a collection instead of Mongo's regular collection.
Because I'm using MongoObservable and trying to invoke .insert() on that, the return is a bit different then a regular Mongo Collection and will return an Observable instead of the Id. Documentation Here
If you add a .collection after the collection name you will get the Id in return like so:
submitStuff: function(data): string{
var loggedInUser = Meteor.user();
if (!loggedInUser || !Roles.userIsInRole(loggedInUser,['user','admin'])){
throw new Meteor.Error("M504", "Access denied");
} else {
try{
let id = new Mongo.ObjectID();
const model: MyModel[] = [{
data.stuff //bunch of data being inserted
_id:
}];
model.forEach((obj: MyModel) => {
insertedData = Collection.collection.insert(obj); //added .collection
});
return insertedData;
} catch(e) {
throw new Meteor.Error(e + e.reason, "|Throw new error|");
}
}
},

Update Array attribute using Mongoose

I am working on a MEAN stack application in which i defined a model using following schema:
var mappingSchema = new mongoose.Schema({
MainName: String,
Addr: String,
Mapping1: [Schema1],
Mappings2: [Schema2]
},
{collection : 'Mappings'}
);
I am displaying all this data on UI and Mapping1 & Mapping2 are displayed in the 2 tables where I can edit the values. What I am trying to do is once I update the values in table I should update them in database. I wrote put() api where I am getting these two updated mappings in the form of object but not able to update it in database. I tried using findAndModify() & findOneAndUpdate() but failed.
Here are the Schema1 & Schema2:
const Schema1 = new mongoose.Schema({
Name: String,
Variable: String
});
const Schema2 = new mongoose.Schema({
SName: String,
Provider: String
});
and my put api:
.put(function(req, res){
var query = {MainName: req.params.mainname};
var mapp = {Mapping1: req.params.mapping1, Mapping2: req.params.mapping2};
Mappings.findOneAndUpdate(
query,
{$set:mapp},
{},
function(err, object) {
if (err){
console.warn(err.message); // returns error if no matching object found
}else{
console.log(object);
}
});
});
Please suggest the best to way update those two arrays.
UPDATE :
I tried this
var mapp = {'Mapping2': req.params.mapping2};
Mappings.update( query ,
mapp ,
{ },
function (err, object) {
if (err || !object) {
console.log(err);
res.json({
status: 400,
message: "Unable to update" + err
});
} else {
return res.json(object);
}
});
what I got is
My array with size 3 is saved as String in Mapping2 array.
Please help. Stuck badly. :(
From Mongoose's documentation I believe there's no need to use $set. Just pass an object with the properties to update :
Mappings.findOneAndUpdate(
query,
mapp, // Object containing the keys to update
function(err, object) {...}
);

Why can't I delete a mongoose model's object properties?

When a user registers with my API they are returned a user object. Before returning the object I remove the hashed password and salt properties. I have to use
user.salt = undefined;
user.pass = undefined;
Because when I try
delete user.salt;
delete user.pass;
the object properties still exist and are returned.
Why is that?
To use delete you would need to convert the model document into a plain JavaScript object by calling toObject so that you can freely manipulate it:
user = user.toObject();
delete user.salt;
delete user.pass;
Non-configurable properties cannot be re-configured or deleted.
You should use strict mode so you get in-your-face errors instead of silent failures:
(function() {
"use strict";
var o = {};
Object.defineProperty(o, "key", {
value: "value",
configurable: false,
writable: true,
enumerable: true
});
delete o.key;
})()
// TypeError: Cannot delete property 'key' of #<Object>
Another solution aside from calling toObject is to access the _doc directly from the mongoose object and use ES6 spread operator to remove unwanted properties as such:
user = { ...user._doc, salt: undefined, pass: undefined }
Rather than converting to a JavaScript object with toObject(), it might be more ideal to instead choose which properties you want to exclude via the Query.prototype.select() function.
For example, if your User schema looked something like this:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
name: {
type: String,
required: true
},
pass: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
module.exports = {
User: mongoose.model("user", userSchema)
};
Then if you wanted to exclude the pass and salt properties in a response containing an array of all users, you could do so by specifically choosing which properties to ignore by prepending a minus sign before the property name:
users.get("/", async (req, res) => {
try {
const result = await User
.find({})
.select("-pass -salt");
return res
.status(200)
.send(result);
}
catch (error) {
console.error(error);
}
});
Alternatively, if you have more properties to exclude than include, you can specifically choose which properties to add instead of which properties to remove:
const result = await User
.find({})
.select("email name");
The delete operation could be used on javascript objects only. Mongoose models are not javascript objects. So convert it into a javascript object and delete the property.
The code should look like this:
const modelJsObject = model.toObject();
delete modlelJsObject.property;
But that causes problems while saving the object. So what I did was just to set the property value to undefined.
model.property = undefined;
Old question, but I'm throwing my 2-cents into the fray....
You question has already been answered correctly by others, this is just a demo of how I worked around it.
I used Object.entries() + Array.reduce() to solve it. Here's my take:
// define dis-allowed keys and values
const disAllowedKeys = ['_id','__v','password'];
const disAllowedValues = [null, undefined, ''];
// our object, maybe a Mongoose model, or some API response
const someObject = {
_id: 132456789,
password: '$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/',
name: 'John Edward',
age: 29,
favoriteFood: null
};
// use reduce to create a new object with everything EXCEPT our dis-allowed keys and values!
const withOnlyGoodValues = Object.entries(someObject).reduce((ourNewObject, pair) => {
const key = pair[0];
const value = pair[1];
if (
disAllowedKeys.includes(key) === false &&
disAllowedValues.includes(value) === false
){
ourNewObject[key] = value;
}
return ourNewObject;
}, {});
// what we get back...
// {
// name: 'John Edward',
// age: 29
// }
// do something with the new object!
server.sendToClient(withOnlyGoodValues);
This can be cleaned up more once you understand how it works, especially with some fancy ES6 syntax. I intentionally tried to make it extra-readable, for the sake of the demo.
Read docs on how Object.entries() works: MDN - Object.entries()
Read docs on how Array.reduce() works: MDN - Array.reduce()
I use this little function just before i return the user object.
Of course i have to remember to add the new key i wish to remove but it works well for me
const protect = (o) => {
const removes = ['__v', '_id', 'salt', 'password', 'hash'];
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
and i use it as I said, just before i return the user.
return res.json({ success: true, user: await protect(user) });
Alternativly, it could be more dynamic when used this way:
const protect = (o, removes) => {
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
return res.json({ success: true, user: await protect(user, ['salt','hash']) });

Categories

Resources