Set sort order for mongoose document - javascript

I have a mongoose schema:
models/profile.js
var mongoose = require("mongoose");
var passportLocalMongoose = require("passport-local-mongoose");
var profileSchema = new mongoose.Schema({
username: String,
complete: { type: Boolean, default: false },
email: { type: String, default: "" },
city: { type: String, default: "" }
}, { discriminatorKey: 'accountType' });
profileSchema.plugin(passportLocalMongoose);
module.exports = mongoose.model('Profile', profileSchema);
That has two discriminators associated with it:
models/finder.js
var Profile = require('./profile');
var Finder = Profile.discriminator('finder', new mongoose.Schema({
position: { type: String, default: "" },
skills: Array
}));
module.exports = mongoose.model("Finder");
models/helper.js
var Profile = require('./profile');
var Helper = Profile.discriminator('helper', new mongoose.Schema({
jobTitle: { type: String, default: "" },
lastPosition: { type: String, default: "" }
}));
module.exports = mongoose.model("Helper");
I am using this within an express framework, and on one page - shown below - I want to iterate over the key/value pairs in Profile to build a table.
I would like to retain the order designated in the Schema, so that the table ordering is consistent between pages.
Is it possible to define a sort order on Schema creation?
Here's my profile.ejs file where I make the table:
<table class="table profile-display">
<tbody>
<% for(var key in profile.toObject({versionKey: false})) { %>
<% if (key != '_id') { %>
<% if (profile[key].length === 0){ %>
<tr class="incomplete-warning table-rows">
<% } else { %>
<tr class="table-rows">
<% } %>
<td class="key-text"><%=key.toUpperCase()%>:</td>
<td><%=profile[key]%></td>
</tr>
<% } %>
<% } %>
</tbody>
</table>
Please let me know if I can provide more information

You can use retainKeyOrder
By default, mongoose reverses key order in documents as a performance optimization. For example, new Model({ first: 1, second: 2 }); would actually be stored in MongoDB as { second: 2, first: 1 }. This behavior is considered deprecated because it has numerous unintended side effects, including making it difficult to manipulate documents whose _id field is an object.
Mongoose >= 4.6.4 has a retainKeyOrder option for schemas that ensures that mongoose will always keep the correct order for your object keys.
var testSchema = new Schema({ first: Number, second: Number }, { retainKeyOrder: true });
var Test = mongoose.model('Test', testSchema);
Test.create({ first: 1, second: 2 }); // Will be stored in mongodb as `{ first: 1, second: 2 }`
References:
https://github.com/Automattic/mongoose/issues/1514
https://mongoosejs.com/docs/4.x/docs/guide.html#retainKeyOrder

Every browser handles object sorts differently. I suggest returning a JSON object which maps your schema that has a sort value or such and iterates over your profile.ejs template. Then you can just map the value from mongoose output key whichever you like.
{
1 : 'username',
2 : 'type',
3 : 'complete',
4 : 'email',
5 : 'city',
6 : 'position',
7 : 'skills'
}
or an array
[
'username',
'type',
'complete',
'email',
'city',
'position',
'skills'
]
Then from that, you can just map your schema from the value of the object or from the array. I like using an array in this case as its easier to iterate over by just using the index key. It depends on your usage and purpose.
Hope it help.
UPDATE: To minimizing in hardcoding the schema twice you can create an object which has sorts with the schema value.
Code Example.
// Construct your object
var objSchema = {
obj1 : {
schema : { type: String },
order : 2
},
obj2 : {
schema : { type: String },
order : 3
},
obj3 : {
schema : { type: String },
order : 1
}
}
// Function to construct your schema object and sort array
function mapSortSchema(objs) {
var result = {
schema : {},
order : []
}
for (var obj in objs) {
result.schema[obj] = objs[obj].schema;
result.order.push({
'key': obj,
'order': objs[obj].order
});
}
result.order.sort(function(a, b) {
return a.order - b.order;
});
return result;
}
Now you have schema for mongoose and order for template.
var mapSchema = mapSortSchema(objSchema);
// You can now use this with your mongoose schema
var forMongooseSchema = mapSchema.schema;
// result {obj1 : { type: String }, obj2 : { type: String }, obj3 : { type: String }}
// You can now use this to loop through your template
var forTemplateLoop = mapSchema.order;
// result [{key: "obj3", order: 1}, {key: "obj1", order: 2}, {key: "obj2", order: 3}]
Haven't tested this in mongoose but it will give you the basic idea, you can improve the function base on your need. Hope it helps.

Related

Node.js: Mongoose filter data from array of ObjectIDs stored in collection [duplicate]

This question already has answers here:
mongodb/mongoose findMany - find all documents with IDs listed in array
(9 answers)
Closed 3 months ago.
I am trying to search using node.js, ejs and mongoose. All the filter parameters are working perfectly but only categoryIds is not (stored as a collection of ObjectIDs in the mongodb document, referring to the respective document in categories collection), always giving me the empty record set.
For example:
If I need to find the a movie called Cosmos (see the attached screenshot) then I can easily find it with all or any filter except categories. Once I select any category, the record-set will go blank even if the I have selected the one which it belongs to.
model.js
const Model = mongoose.model('Movie', new Schema({
...
categoryIds: [{
type: Schema.Types.ObjectId,
trim: true,
default: null,
ref: 'Category',
}],
copyrightId: {
type: Schema.Types.ObjectId,
trim: true,
default: null,
ref: 'Copyright',
},
...
}, {
timestamps: true
});
Controller.js
Router.get('/', (req, res) => {
const search = req.query;
const conditions = (() => {
let object = {};
['releaseYear', 'languageId', 'copyrightId'].forEach(filter => {
if (search[filter] != '') {
object[filter] = search[filter];
}
});
if (typeof search.categoryIds !== 'undefined') {
object.categoryIds = [];
search.categoryIds.forEach(item => object.categoryIds.push(item));
}
if (search.keywords != '') {
object.title = {
$regex: search.keywords,
$options: 'i'
};
}
return object;
})();
const count = await Model.count(conditions);
const items = await Model.find(conditions, {
__v: false,
imdb: false,
trailer: false,
createdAt: false,
updatedAt: false,
}).sort({
status: -1,
releaseYear: -1,
title: 1
})
.populate('languageId', ['title'])
.populate('copyrightId', ['title'])
.populate('categoryIds', ['title'])
.skip(serialNumber)
.limit(perPage);
...
});
All the fields in the search form
{
categoryIds: [
'6332a8a2a336e8dd78e3fe30',
'6332a899a336e8dd78e3fe2e',
'6332a87ba336e8dd78e3fe2c',
'634574ab339b1a6b09c1e144'
],
languageId: '',
copyrightId: '',
releaseYear: '',
rating: '',
seen: '',
status: '',
keywords: '',
submit: 'search' // button
}
filtered search parameters
{
categoryIds: [
'6332a8a2a336e8dd78e3fe30',
'6332a899a336e8dd78e3fe2e',
'6332a87ba336e8dd78e3fe2c',
'634574ab339b1a6b09c1e144'
]
}
Here is the screenshot of mongodb document.
...
if (typeof search.categoryIds !== 'undefined') {
object.categoryIds = {
$in: []
};
search.categoryIds.forEach(item => object.categoryIds.$in.push(
mongoose.Types.ObjectId(item))
);
}
console.log(object);
return object;
The is the final filter object
{
categoryIds: {
'$in': [
new ObjectId("6332a87ba336e8dd78e3fe2c"),
new ObjectId("634669f4a2725131e80d99f1")
]
}
}
Now, all the filters are working perfectly.
Thank you everyone.
The filter should contain all categoryIds and in the same order to match the document. It's not quite clear from the question if it is the intended functionality. If not, most popular usecases are documented at https://www.mongodb.com/docs/manual/tutorial/query-arrays/
I don't recall how mongoose handles types when you query with array function like $all, so you may need to convert string IDs to ObjectIDs manually, e.g.:
search.categoryIds.forEach(item => object.categoryIds.push(
mongoose.Types.ObjectId(item))
);

How to allow a mongooses schema Schema.Types.ObjectId to be empty when filling

I've a mongoose schema called Route as the following:
const mongoose = require("mongoose");
const collection_label = "routes";
const class_label = "Routes";
const schema = mongoose.Schema(
{
createdOn: { type: Date, default: Date.now },
attachedSubTasks: [
{ type: Schema.Types.ObjectId, ref: "Subtasks", default: "" },
],
},
{ collection: class_label }
);
The fact is that sometime the field can be an empty array, but when I want to create a Route i get the following error:
ValidationError: Routes validation failed: attachedSubTasks: Cast to [ObjectId] failed for value "[""]" at path "attachedSubTasks"
Is there a way to accept the fact that this can be empty?
I tried without default, with default: "" and with default: null
You need to set default as null in your schema not ""
you have also this way to define attachedSubTasks in schema:
attachedSubTasks: {
type: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Subtasks"
}
],
default: []
}
The work around I've found for this problem can be implemented at the backend side. In the following example, I've just check whether the object's key value's length is less than one (1) if true I delete the object's key so that mongoose's schema does not take this into consideration
router.post("/", async (c, r) => {
let receivedData = c.body;
receivedData["attachedSubTasks"].length < 1? delete receivedData["attachedSubTasks"]: false
returnedValue = await mongoose.saveOne(Routes, receivedData);
statRes.status(200, r, returnedValue);
});
If someone has a better solution, don't hesitate to let me know.

Creating a document with { strict: false } in mongoose

The task is to store some documents into MongoDB. The documents have the same top-level but from there they could be different.
The structure of the payload is:
{
"types": "a", //the type can be "a", "b" or "c"
"details" : {
... // the details object structure is different for each type
}
}
and this is the model I wrote:
const Details = { strict: false };
const MyOrder = new Schema({
types: {
type: String,
enum: ['a', 'b', 'c'],
},
details: Details,
});
module.exports = Order = mongoose.model('myOrder', MyOrder);
I set details with { strict: false } because I want to get its data no matter what structure it has. Maybe it's wrong something there.
When a POST request is done, the document saved into the database it looks like this:
_id: ObjectId("...")
types: "a"
__v : 0
It saved the types but nothing about the details.
Is it a way to save the details too?
I managed to solve the problem by not creating another Details object like above but adding { strict: false } inside the Schema. Like this:
const MyOrder = new Schema(
{
types: {
type: String,
enum: ['a', 'b', 'c'],
},
},
{ strict: false }
);
module.exports = Order = mongoose.model('myOrder', MyOrder);

Update multiple documents in MongoDB by field's current value [duplicate]

This question already has answers here:
How to increment a field in mongodb?
(1 answer)
How to decrement like $dec?
(6 answers)
Closed 3 years ago.
I have an Employees Schema :
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const EmployeesSchema = new Schema({
EmployeeId: {
type: String,
required: true
},
// .... more attributes
Copies: {
type: Number,
required: true
InsertDate: {
type: Date,
default: Date.now
}
});
module.exports = Employees = mongoose.model(
"employees",
EmployeesSchema
);
and ids array ...
const _ids = // I get this one from some code manipulation - return the `_id` attribute
and I want to update those documents that are in _ids , by one attribute - Copies :
await Employees.update(
{
_id: {
$in: _ids
}
},
{
$set: { Copies: Copies - 1 } // here it looks for a var called Copies
},
{ multi: true }
);
How can I reduce 1 from Copies attribute , based on each document's Copies value ?

How can I query a nested document and return attributes from it?

I have a Product collection and every document looks something like this:
"_id" : ObjectId("574393c59afcfdd3763d91b1"),
"name" : "dummy product name",
"price" : 200,
"category" : ObjectId("574393c59afcfdd3763d91ad"),
"images" : [ ],
"ratings" : [
2
],
"reviews" : [ ],
"purchase_count" : 0,
"tags" : [
ObjectId("574393c59afcfdd3763d91a9"),
ObjectId("574393c59afcfdd3763d91ab")
]
Where category and tags are references to their respective collections and they each have a name attribute. Here are all 3 schemas:
var ProductSchema = new mongoose.Schema({
name : {type: String, required: true},
price : { type: Number,required: true },
tags : [{ type: Schema.Types.ObjectId, ref: 'Tag' }],
purchase_count : {type: Number, default: 0},
reviews : [ReviewSchema],
ratings : [{type: Number, min: 0, max: 10}],
category : {type: Schema.Types.ObjectId, ref: 'ProductCategory'},
images : [{type: String}]
});
var ProductCategorySchema = new mongoose.Schema({
name : { type: String, required: true, unique: true, dropDups: true }});
var TagSchema = new mongoose.Schema({
name : { type: String, required: true, unique: true, dropDups: true, lowercase: true }
});
I'm trying to query the product collection and return data of each product like this:
app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category').populate('tags').exec();
The only the difference is that I want the actual name of the category instead of a document representing the collection with it's ID. I also want the same for the tags , I just want a simple array of strings which represent the tag names. How can I do this in the query?
I tried implicitly defining what I wanted to select by using a select() after the second populate() and writing 'category.name tags.name (and then all the other attributes)' but that didn't work.
This is what the result of the query looks like:
This is how I got it to work. Credit goes to Neta Meta for giving me the idea of using pure JS.
app.models.Product.find({})
.populate('category' , 'name')
.populate('tags', 'name').populate('seller', 'name').exec(function(err, products){
if (err) throw err;
products = products.map(function(a){
var productToSend = {};
productToSend.name = a.name;
productToSend.price = a.price;
productToSend.description = a.description;
if(a.seller != undefined){
productToSend.seller = a.seller.name;
}
if(a.tags != undefined){
productToSend.tags = a.tags.map(function(b){
return b.name;
});
}
productToSend.purchase_count = a.purchase_count;
productToSend.reviews = a.reviews;
productToSend.ratings = a.ratings;
if(a.category != undefined){
productToSend.category = a.category.name;
}
productToSend.images = a.images;
return productToSend;
})
res.json(products);
});
To select specific field you would
app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category', 'fieldname1 fieldname2').populate('tags', 'fieldname1 fieldname2').exec();
http://mongoosejs.com/docs/populate.html
However i dont see in your schema definition any reference to anything.
Also if you have / need any kind of relation (which is what you seem to need) dont use mongo.
Actually dont use mongo ever unless you absolutely must - try to stay a way from mongo.
update
Something like that.
var res = app.models.Product.find({}, function(err, products){
if (err) throw err;
res.json(products);
}).populate('category', 'fieldname1 fieldname2').populate('tags', 'fieldname1 fieldname2').exec();
res.category = categoty.fieldname;
res.tags = res.tags.map(function(tag){
return tag.fieldname
})
Another option might be - to add getter - not sure if this will work because populate might happened after so ..
but :
ProductSchema.virtual('categoryString').get(function () {
return this.category.fieldname
});
ProductSchema.virtual('tagsArr').get(function () {
return this.tags.map(function(tag){
if(tag && tag.name) {
return tag.fieldname;
}
return '';
});
});
I don't know why you send response before populate query. Using lodash package to modified your products before response.
var _ = require('lodash');
app.models.Product.find({}).populate('category tags').exec(function(err, products){
if (err) throw err;
var modified = _.map(products, function (product) {
product.tags = _.map(product.tags, function (tag) { return _.pick(tag, ['name']) });
product.category = _.pick(product.category, ['name']);
return product;
});
res.json(modified);
});

Categories