I need to be able to increment and decrement the position of an element of an array in a MongoDB object.
I looked at the <update> API in the MongoDB API but could not find anything to let me do so.
I am trying to use findOneAndUpdate through Mongoose and I know the index of the element I am trying to shift up or down.
An example of the array item of base64 encoded images:
{
images: [
"img1",
"img2",
"img3"
]
}
And I would like to move, for example "img2", up or down (but "image" should not be able to pushed up since there is nowhere to go).
If I wanted to push "img2" up then the result would be:
{
images: [
"img2",
"img1",
"img3"
]
}
It doesn't matter if I achieve this through changing the index, swapping, or pushing up/down.
Like #blakes-seven said, you have two ways to do it:
Grabing, updating and pushing
db.images.find({ _id: '' }, { images : 1 })
.then(function(img) {
var tmpImg = img.images[0];
img.images[0] = img.images[1];
img.images[1] = tmpImg;
db.images.update({ _id: img._id }, { $set: { images: img.images } });
})
Updating directly (if you have the image on the client and know the indexes), using $position
db.images.update({ _id: '' }, { $push: { images: { $each: ['img2'] }, $position: 0 } } } )
.then(function() {
db.images.update({ _id: '' }, {$unset: {'images.2': 1}})
});
https://docs.mongodb.org/manual/reference/operator/update/position/
However, I think you should redesign the way you stores your images, using by example a virtual ordering:
{
images: [
{ base64: 'img1', order: 0, _id: 'img1' },
{ base64: 'img2', order: 1, _id: 'img2' },
{ base64: 'img3', order: 2, _id: 'img3' }
]
}
This way, you could order your images using a virtual order index, updating them using only the _id, or updating the whole collection by changing order of all img2, dropping or replacing an image, etc.
Related
I am trying to make a tree table reorderable via drag and drop.
Here's the render for easier visualization:
I am also using React-DnD.
Here's a piece of my code:
const moveRow = useCallback(
(record: StructureElement) => (dragIndex, hoverIndex) => {
// console.log(record.name, dragIndex, hoverIndex);
const dragRow = sortableData[dragIndex];
if (!dragRow) {
console.log(sortableData, dragIndex);
} else {
console.log('drag', dragRow.name, 'hover', sortableData[hoverIndex].name);
}
// todo - change the order in array
},
[sortableData],
);
<StyledTreeTable
components={components}
dataSource={sortableData}
onRow={(record, index) => {
return {
index,
moveRow: moveRow(record as StructureElement),
};
}}
/>
So the problem is, my sortableData looks like this:
[
{ name: 1 },
{ name: 2 },
{
name: 3,
children: [
{ name: 11 },
{ name: 22 },
{ name: 33, children: [{ name: 111 }, { name: 222 }, { name: 333 }] },
],
},
];
So my objects can have more objects nested in children.
But moveRow function doesn't see it this way. For it, ABSTRACT_STATE_4 is of index 4. So when I try to reorder the array, I try to do this: data[4], which results in undefined.
Is there a way I can reorder the items in tree table with drag and drop, that I haven't found yet?
The only way I see of solving it now, is to recreate the array the way hover and drag indexes match. But that's a lot of seemingly unnecessary work.
Is there a way to get the record I am hovering over, like I'm getting hoverIndex?
I am working on an express js application where I need to update a nested array.
1) Schema :
//Creating a mongoose schema
var userSchema = mongoose.Schema({
_id: {type: String, required:true},
name: String,
sensors: [{
sensor_name: {type: String, required:true},
measurements: [{time: String}]
}] });
2)
Here is the code snippet and explanation is below:
router.route('/sensors_update/:_id/:sensor_name/')
.post(function (req, res) {
User.findOneAndUpdate({_id:req.body._id}, {$push: {"sensors" :
{"sensor_name" : req.body.sensor_name , "measurements.0.time": req.body.time } } },
{new:true},function(err, newSensor) {
if (err)
res.send(err);
res.send(newSensor)
}); });
I am able to successfully update a value to the measurements array using the findOneAndUpdate with push technique but I'm failing when I try to add multiple measurements to the sensors array.
Here is current json I get if I get when I post a second measurement to the sensors array :
{
"_id": "Manasa",
"name": "Manasa Sub",
"__v": 0,
"sensors": [
{
"sensor_name": "ras",
"_id": "57da0a4bf3884d1fb2234c74",
"measurements": [
{
"time": "8:00"
}
]
},
{
"sensor_name": "ras",
"_id": "57da0a68f3884d1fb2234c75",
"measurements": [
{
"time": "9:00"
}
]
}]}
But the right format I want is posting multiple measurements with the sensors array like this :
Right JSON format would be :
{
"_id" : "Manasa",
"name" : "Manasa Sub",
"sensors" : [
{
"sensor_name" : "ras",
"_id" : ObjectId("57da0a4bf3884d1fb2234c74"),
"measurements" : [
{
"time" : "8:00"
}
],
"measurements" : [
{
"time" : "9:00"
}
]
}],
"__v" : 0 }
Please suggest some ideas regarding this. Thanks in advance.
You might want to rethink your data model. As it is currently, you cannot accomplish what you want. The sensors field refers to an array. In the ideal document format that you have provided, you have a single object inside that array. Then inside that object, you have two fields with the exact same key. In a JSON object, or mongo document in this context, you can't have duplicate keys within the same object.
It's not clear exactly what you're looking for here, but perhaps it would be best to go for something like this:
{
"_id" : "Manasa",
"name" : "Manasa Sub",
"sensors" : [
{
"sensor_name" : "ras",
"_id" : ObjectId("57da0a4bf3884d1fb2234c74"),
"measurements" : [
{
"time" : "8:00"
},
{
"time" : "9:00"
}
]
},
{
// next sensor in the sensors array with similar format
"_id": "",
"name": "",
"measurements": []
}],
}
If this is what you want, then you can try this:
User.findOneAndUpdate(
{ _id:req.body._id "sensors.sensor_name": req.body.sensor_name },
{ $push: { "sensors.0.measurements": { "time": req.body.time } } }
);
And as a side note, if you're only ever going to store a single string in each object in the measurements array, you might want to just store the actual values instead of the whole object { time: "value" }. You might find the data easier to handle this way.
Instead of hardcoding the index of the array it is possible to use identifier and positional operator $.
Example:
User.findOneAndUpdate(
{ _id: "Manasa" },
{ $push: { "sensors.$[outer].measurements": { "time": req.body.time } } }
{ "arrayFilters:" [{"outer._id": ObjectId("57da0a4bf3884d1fb2234c74")}]
);
You may notice than instead of getting a first element of the array I specified which element of the sensors array I would like to update by providing its ObjectId.
Note that arrayFilters are passed as the third argument to the update query as an option.
You could now make "outer._id" dynamic by passing the ObjectId of the sensor like so: {"outer._id": req.body.sensorId}
In general, with the use of identifier, you can get to even deeper nested array elements by following the same procedure and adding more filters.
If there was a third level nesting you could then do something like:
User.findOneAndUpdate(
{ _id: "Manasa" },
{ $push: { "sensors.$[outer].measurements.$[inner].example": { "time": req.body.time } } }
{ "arrayFilters:" [{"outer._id": ObjectId("57da0a4bf3884d1fb2234c74"), {"inner._id": ObjectId("57da0a4bf3884d1fb2234c74"}}]
);
You can find more details here in the answer written by Neil Lunn.
refer ::: positional-all
--- conditions :: { other_conditions, 'array1.array2.field_to_be_checked': 'value' }
--- updateData ::: { $push : { 'array1.$[].array2.$[].array3' : 'value_to_be_pushed' } }
var model = {
cats: [
{
clickCount: 0,
name: "Panther",
imgUrl: "images/cat1.png"
},
{
clickCount: 0,
name: "Tiger",
imgUrl: "images/cat2.png"
},
{
clickCount: 0,
name: "Rocky",
imgUrl: "images/cat3.png"
},
{
clickCount: 0,
name: "Marshal",
imgUrl: "images/cat4.png"
},
{
clickCount: 0,
name: "Simpson",
imgUrl: "images/cat5.png"
},
{
clickCount: 0,
name: "Kajol",
imgUrl: "images/cat6.png"
}
]
};
catObject = {
name: document.getElementById('text-name').value,
url: document.getElementById('text-url').value,
count: document.getElementById('text-count').value
};
model is an array of objects. I am trying to add another object using model.cats.push(catObject);
where text-name, text-url, text-count are ids of textboxes in a form.
I am not getting desired results. I am seeing that only catObject.name is getting pushed onto model.cats and catObject.url and catObject.count is not getting added and hence 'undefined' are values of the model.cats.imgUrl and model.cats.clickCount.
I can add full code if required. I tried so may things.. Could someone check if I am missing anything here? Thanks in advance.
Correction
model is not an array of objects rather model is an object that contains a property (i.e. cats) which is an array of objects.
You're correct
From your description, it is clear that you have successfully pushed catObject into cats using model.cats.push(catObject);
What went wrong
You might have used loop to access the elements inside cats array and got undefined for model.cats.clickCount and model.cats.imgUrl of only the last object you've pushed because you named properties of catObject as url and count (i.e. instead of imgUrl and clickCount). So, just change the property names to imgUrl and clickCount to fix the issue.
Corrected code here
I'm creating a recipe-database (commonly known as a cookbook) where I need to have a many-to-many relationship between ingredients and recipes and I'm using sequelize.js in combination with postgresql.
When an ingredient is added to a recipe I need to declare the correct amount of that ingredient that goes into the recipe.
I've declared (reduced example)
var Ingredient = sequelize.define('Ingredient', {
name: Sequelize.STRING
}, {
freezeTable: true
});
var Recipe = sequelize.define('Recipe', {
name: Sequelize.STRING
}, {
freezeTable: true
});
var RecipeIngredient = sequelize.define('RecipeIngredient', {
amount: Sequelize.DOUBLE
});
Ingredient.belongsToMany(Recipe, { through: RecipeIngredient });
Recipe.belongsToMany(Ingredient, {
through: RecipeIngredient,
as: 'ingredients'
});
My problem is with how data is returned when one my REST endpoints do
router.get('/recipes', function(req, res) {
Recipe.findAll({
include: [{
model: Ingredient,
as: 'ingredients'
}]
}).then(function(r) {
return res.status(200).json(r[0].toJSON());
})
});
The resulting JSON that gets sent to the client looks like this (timestamps omitted):
{
"id": 1,
"name": "Carrots",
"ingredients": [
{
"id": 1,
"name": "carrot",
"RecipeIngredient": {
"amount": 12,
"RecipeId": 1,
"IngredientId": 1
}
}
]
}
While all I wanted was
{
"id": 1,
"name": "Carrots",
"ingredients": [
{
"id": 1,
"name": "carrot",
"amount": 12,
}
]
}
That is, I want the amount field from the relation-table to be included in the result instead of the entire RecipeIngredient object.
The database generated by sequelize looks like this:
Ingredients
id name
1 carrot
Recipes
id name
1 Carrots
RecipeIngredients
amount RecipeId IngredientId
12 1 1
I've tried to provide an attributes array as a property to the include like this:
include: [{
model: Ingredient,
as: 'ingredients',
attributes: []
}]
But setting either ['amount'] or ['RecipeIngredient.amount'] as the attributes-value throws errors like
Unhandled rejection SequelizeDatabaseError: column ingredients.RecipeIngredient.amount does not exist
Obviously I can fix this in JS using .map but surely there must be a way to make sequelize do the work for me?
I am way late to this one, but i see it been viewed quite a bit so here is my answer on how to merge
attributes
Some random examples in this one
router.get('/recipes', function(req, res) {
Recipe.findAll({
include: [{
model: Ingredient,
as: 'ingredients',
through: {
attributes: ['amount']
}
}]
})
.then(docs =>{
const response = {
Deal: docs.map(doc =>{
return{
cakeRecipe:doc.recipe1,
CokkieRecipe:doc.recipe2,
Apples:doc.ingredients.recipe1ingredient
spices:[
{
sugar:doc.ingredients.spice1,
salt:doc.ingredients.spice2
}
]
}
})
}
})
res.status(200).json(response)
})
You can use sequelize.literal. Using Ingredient alias of Recipe, you can write as follows. I do not know if this is the right way. :)
[sequelize.literal('`TheAlias->RecipeIngredient`.amount'), 'amount'],
I tested with sqlite3. Received result with alias "ir" is
{ id: 1,
name: 'Carrots',
created_at: 2018-03-18T04:00:54.478Z,
updated_at: 2018-03-18T04:00:54.478Z,
ir: [ { amount: 10, RecipeIngredient: [Object] } ] }
See the full code here.
https://github.com/eseom/sequelize-cookbook
I've gone over the documentation but I couldn't find anything that seems like it would let me merge the attributes of the join-table into the result so it looks like I'm stuck with doing something like this:
router.get('/recipes', function(req, res) {
Recipe.findAll({
include: [{
model: Ingredient,
as: 'ingredients',
through: {
attributes: ['amount']
}
}]
}).then(function(recipes) {
return recipes[0].toJSON();
}).then(function(recipe) {
recipe.ingredients = recipe.ingredients.map(function(i) {
i.amount = i.RecipeIngredient.amount;
delete i.RecipeIngredient;
return i;
});
return recipe;
}).then(function(recipe) {
return res.status(200).json(recipe);
});
});
Passing through to include lets me filter out which attributes I want to include from the join-table but for the life of me I could not find a way to make sequelize merge it for me.
The above code will return the output I wanted but with the added overhead of looping over the list of ingredients which is not quite what I wanted but unless someone comes up with a better solution I can't see another way of doing this.
In Node and Mongoose, I'd like to remove an object from an array in a document. The structure is like this.
{
_id: ObjectId,
title: String,
tags: [
{ text: String },
{ text: String }
]
}
I will look up an item by it's _id, but then I want to look within the tags for a certain String and remove that from the array.
You effectively want to update the document and use the $pull operator with a query matching the matching value under "tags.tag":
Model.update(
{ "_id": docId, "tags.tag": "mytag" },
{ "$pull": { "tags": { "tag": "mytag" } },
function(err,numAffected) {
}
)