Mongoose populate denormalized data - javascript

I have a User model and a Book model. I want some data from my books to be denormalized on each User document, but still have the option to populate if needed. If I set ref: 'Book' on the books.$._id it gets populated inside the _id path which is unintended. I would like the population to overwrite the denormalized data.
How do I accomplish this?
in users.model.js:
const { Schema } = require('mongoose');
const UserSchema = new Schema({
name: String,
books: {
type: [
{
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
],
default: [],
},
});
Desired outcome
in users.controller.js:
app.get('/', async (req, res, next) => {
const users = await User.find({})
/*
users: [{
_id: ObjectId(),
name: 'Andrew',
books: [{
_id: ObjectId(),
title: 'Game of Thrones',
length: 298,
}, { ... }],
}, { ... }]
*/
});
app.get('/:id', async (req, res, next) => {
const book_id = req.params.id;
const user = await User.findById(book_id).populate({
path: 'books',
model: 'Book',
});
/*
user: {
_id: ObjectId(),
name: 'Andrew',
books: [{
_id: ObjectId(),
name: 'Game of Thrones',
length: 298,
author: 'Simone Dunow',
releasedOn: Date(),
price: 30,
...
}, { ... }],
}
*/
});
Schemas I've tried so far:
books: {
type: [
{
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
],
default: [],
ref: 'Book',
},
returns array of { _id: null }
books: {
type: [
{
_id: {
type: Schema.Types.ObjectId,
ref: 'Book',
},
title: String,
length: Number,
},
],
default: [],
},
books are populated inside of _id: { _id: { Book } }
books: {
type: [
{
type: {
_id: Schema.Types.ObjectId,
title: String,
length: Number,
},
ref: 'Book',
},
],
default: [],
},
throws exception: invalid type

const UserSchema = new Schema({
name: String,
books: [{
id: { type : Schema.Types.ObjectId, ref : 'Book'} //Whatever string you have used while modeling your schema
title: String,
length: Number,
}],
});
While using the schema you can populate as follows :
populate({ path: 'books.id' })
Output :
{
_id : // someid
name : "somename"
books : [
{
id : {//document referring to Books collection},
title : "sometitle",
length : //somelength
}, ...
]
}

To anybody that might be still looking to achieve a full replacement, full disclosure: It might be a bit hacky for some evangelists or even have a performance toll on high traffic apps, but if you really want to do it, you can tap into the toJSON method of the schema like the following:
UserSchema.method('toJSON', function () {
let obj = this.toObject();
obj.books = obj.books.map(
(book) => (Schema.Types.ObjectId.isValid(book.id)) ? book : book.id
);
return obj;
});
What's going on here is basically we're replacing the whole property with the populated result when the book.id has been populated otherwise we just return the original object by checking the validity of the book's id (when populated will be a full bloomed object rather than an id).

Related

Mongoose One-to-Many, How to get field value of category

I have 2 collection: Category and book
Book:
const mongoose = require('mongoose');
// eslint-disable-next-line camelcase
const mongoose_delete = require('mongoose-delete');
const { Schema } = mongoose;
const SchemaTypes = mongoose.Schema.Types;
const Book = new Schema({
name: { type: String },
price: { type: Number },
images: { type: Array },
country: { type: String },
author: { type: String },
publicationDate: { type: String },
description: { type: String },
category: { type: SchemaTypes.ObjectId, ref: 'Category' },
date: { type: Date, default: Date.now },
}, {
timestamps: true,
});
Book.plugin(mongoose_delete);
Book.plugin(mongoose_delete, { overrideMethods: 'all', deletedAt: true });
module.exports = mongoose.model('Book', Book);
Category:
const mongoose = require('mongoose');
// eslint-disable-next-line camelcase
const mongoose_delete = require('mongoose-delete');
const SchemaTypes = mongoose.Schema.Types;
const { Schema } = mongoose;
const Category = new Schema({
name: { type: String, required: true },
image: { type: String },
products: [{ type: SchemaTypes.ObjectId, ref: 'Book' }],
date: { type: Date, default: Date.now },
}, {
timestamps: true,
});
Category.plugin(mongoose_delete);
Category.plugin(mongoose_delete, { overrideMethods: 'all', deletedAt: true });
module.exports = mongoose.model('Category', Category);
and I select all the books received on the list:
{
_id: new ObjectId("623668ac5b1d392b37690cbc"),
name: 'The Godfather',
price: 110000,
country: 'U.S',
publicationDate: '1969',
category: new ObjectId("6238fdf64f60303756b60b20"),
author: 'Mario Puzo',
... : ...
}
And I want to display the category name on the product list, how do I do it?
**like: {{book.category.name}}
{{book.category.image}}
?**
Since you are using Mongoose, you can use populate() method. You can do it like this:
const books = await Books.find().populate('category')
try $lookup (aggregation)
Book.aggregate([{
$lookup: {
from: "Category",
localField: "category",
foreignField: "_id",
as: "category"
}
}]).exec(function(err, students) {
});

How to populate mongoose with obejectId which is defined as Number?

I'm trying to populate my user requests.profileId but it returns only nulls.
I have the following schemas:
First Schema:
const profileSchema = new mongoose.Schema({
_id: { type: Number }, //<- _id is defined as a number which represents mobile number (easier for me to handle)
first: { type: String },
second: { type: String },
});
module.exports = mongoose.model('Profile', profileSchema, 'profiles');
Second Schema:
const userSchema = new mongoose.Schema({
firstName: { type: String },
lastName: { type: String },
requests: [
{
profileId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Profile',
},
requestTime: { type: Date, default: Date.now },
},
],
});
module.exports = mongoose.model('User', userSchema, 'users');
Here is my code:
const user = await User.findById(req.user).populate('requests.profileId');
console.log(user.requests);
Here is the output:
[
{
_id: 6201633869648e2b74c00a10,
profileId: null,
requestTime: 2022-02-07T18:21:44.722Z
},
{
_id: 6201633b69648e2b74c00a11,
profileId: null,
requestTime: 2022-02-07T18:21:47.238Z
},
{
_id: 620238f9d2b5dd3dee6c41a2,
profileId: null,
requestTime: 2022-02-08T09:33:45.176Z
},
{
_id: 620239253220343dfd7cfdd9,
profileId: null,
requestTime: 2022-02-08T09:34:29.780Z
}
]
Here is the output without populate:
[
{
_id: 6201633869648e2b74c00a10,
profileId: 393732353235303134343330, //<- typeof profileId is obeject
requestTime: 2022-02-07T18:21:44.722Z
},
{
_id: 6201633b69648e2b74c00a11,
profileId: 393732353435353333313131,
requestTime: 2022-02-07T18:21:47.238Z
},
{
_id: 620238f9d2b5dd3dee6c41a2,
profileId: 393732353435353333313131,
requestTime: 2022-02-08T09:33:45.176Z
},
{
_id: 620239253220343dfd7cfdd9,
profileId: 393732353435353333313131,
requestTime: 2022-02-08T09:34:29.780Z
}
]
Currently Profile.findById(mobileNumber) works fine.
Any ideas what went wrong?
Will greatly appreciate your assistance.
Thanks in advance :)
Try this might work let me know if it doesn't
const user = await User.findById(req.user).populate('Profile');
console.log(user.requests);
try this:
User.findById(req.user).populate({
path: 'requests',
populate: {
path: 'profileId',
model: 'Profile'
}
})
For future readers having the same issue!
I've found a solution for this issue.
I had to change the profileId type to Number in my userSchema:
const userSchema = new mongoose.Schema({
firstName: { type: String },
lastName: { type: String },
requests: [
{
profileId: {
type: Number // and NOT use type: mongoose.Schema.Types.ObjectId,
ref: 'Profile',
},
requestTime: { type: Date, default: Date.now },
},
],
});
module.exports = mongoose.model('User', userSchema, 'users');
Now it works!

Mongoose: Access parent key from child object in Schema

I am working with mongoose schema and trying to get a parent obj in child from the child itself (I know it is not allowed in Javascript, but there are workarounds). This was my first implementation
const customer = mongoose.Schema({
name: String,
products_sold: [
{
name: String,
price: Number,
qty: Number,
},
{
name: String,
price: Number,
qty: Number,
},
],
messages: [
{
timestamp: {
type : Date,
default: Date.now
},
_my_key_: {
type: String,
default: () => {
// here i need to get products_sold.name in array like [products_sold[0].name, products_sold[1].name]
// this.products_sold does not work
},
},
}
]
})
I looked up some resources like this one. So i also tried
const customer = mongoose.Schema({
name: String,
products_sold: [
{
name: String,
price: Number,
qty: Number,
},
{
name: String,
price: Number,
qty: Number,
},
],
messages: [
{
timestamp: {
type : Date,
default: Date.now
},
_my_key_: {
type: String,
default: () => {
// here this.parent.products_sold does not work also
},
},
}
],
init: function(){
this.messages._my_key_.parent = this;
delete this.init;
return this;
}
}.init()
)
For Reference:
Mongoose Default Functions and This
This question does not answer mine.
EDIT # 1
I tried this with both arrow and regular function.
EDIT # 2
As per comment feedback from #Molda. After the above code, This is how i make the instance and save a record.
const Customer = mongoose.model('Customer', customer);
const customer = {
name: "John Doe",
products_sold: [
{
name: "product_name",
price: 1245,
qty: 2,
}
],
messages: [
{
// message timestamp will generate from default and _my_key_ too will generate from default
}
]
}
const callingFunc = async () => {
const cust = await Customer(customer);
await cust.save();
return cust;
};

Is To-One Relationships in Realm JavaScript only for one schema?

I am trying to insert my nested object to Realm with To-One Relationships method, but I got an unexpected result where all value of my nested object is the same thing as the value from the first of my nested object that has been Relationship
This is my schema looks like
const PhotoSchema = {
name: 'CUSTOMER_PHOTOS',
properties: {
base64: 'string'
}
};
const TimeSchema = {
name: 'CUSTOMER_TIMES',
properties: {
warranty: 'float',
finish: 'float'
}
};
const MainSchema = {
name: 'CUSTOMERS',
primaryKey: 'id',
properties: {
id: 'int',
name: 'string',
photo: {type: 'CUSTOMER_PHOTOS'},
time: {type: 'CUSTOMER_TIMES'},
}
};
And try to insert some data like this
import Realm from 'realm';
Realm.open({
path: 'mydb.realm',
schema: [PhotoSchema, TimeSchema, MainSchema]
})
.then((realm) => {
realm.write(() => {
realm.create('CUSTOMERS', {
id: Date.now(),
name: 'John',
photo: {
base64: 'ImageBase64'
},
time: {
warranty: 31,
finish: 7
}
})
})
})
.catch((error) => {
console.error(error)
});
The process of inserting data is successfully BUT I got unexpected result when successfully get that data from Realm
Unexpected Result in console.log()
{
id: 1601335000882,
name: "John",
photo: {
base64: "ImageBase64"
},
// This value is the same as PhotoSchema
time: {
base64: "ImageBase64"
}
}
I want to the actual result like this
{
id: 1601335000882,
name: "John",
photo: {
base64: "ImageBase64"
},
time: {
warranty: 21
finish: 7
}
}
I there anything wrong with my code? The Documentation is not too detail about the method, the explanation and the example is just like one word
UPDATE:
I got an unexpected result only in the console.log() and if I try to access the property directly like MY_DATA.time.warranty the result is what I expected
The Answer is: No
To-One Relationships method is not only for one Schema, and Thanks to Angular San for showing an example of Inverse Relationships method.
Try Inverse Relationships
I got an expected result with Inverse Relationships method. In this method, you have to add one property that connected to Main Schema and I want to call this a combiner property
const PhotoSchema = {
name: 'CUSTOMER_PHOTOS',
properties: {
base64: 'string',
combiner: {type: 'linkingObjects', objectType: 'CUSTOMERS', property: 'photo'}
}
};
const TimeSchema = {
name: 'CUSTOMER_TIMES',
properties: {
warranty: 'float',
finish: 'float',
combiner: {type: 'linkingObjects', objectType: 'CUSTOMERS', property: 'time'}
}
};
const MainSchema = {
name: 'CUSTOMERS',
primaryKey: 'id',
properties: {
id: 'int',
name: 'string',
photo: 'CUSTOMER_PHOTOS',
time: 'CUSTOMER_TIMES',
}
};

Mongoose populate virtuals - exclude localField and foreignField from the query results if foreignField is null or an empty array

I have a simple question.
In Mongoose I have two models. One is for Leagues and the other one is for Fixtures. Each league has a unique id, and each fixture is related to a league with it's key of league_id.
In the Leagues schema I have also defined a virtual called fixtures.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const LeagueSchema = new Schema(
{
_id: { type: mongoose.Schema.Types.ObjectId, select: false, auto: true },
id: { type: Number, required: true, unique: true, index: true },
name: { type: String, required: true },
}
);
LeagueSchema.virtual('fixtures', {
ref: 'fixtures',
localField: 'id',
foreignField: 'league_id',
justOne: false,
});
const Leagues = mongoose.model('leagues', LeagueSchema);
const FixtureSchema = new Schema(
{
_id: { type: mongoose.Schema.Types.ObjectId, select: false },
match_id: { type: Number, required: true, index: true, unique: true },
league_id: { type: Number, required: true },
league_name: { type: String, required: true },
timestamp: { type: Number, required: true, index: true },
}
);
const Fixtures = mongoose.model('fixtures', FixtureSchema);
Here is the problem. The list of all leagues is very big and not all of them will always have fixtures that match the .populate() so I want to exclude them from my query.
For example, if I don't want any fixtures older than a given timestamp, I will do the following:
(async () => {
const target_time = 1568851200000;
const leagues = await Leagues
.find({})
.populate({ path: 'fixtures', match: { timestamp: { $gte: target_time } } })
.lean();
console.log(leagues);
})();
What this does is it filters out the fixtures by timestamp correctly, but it does not exclude Leagues without fixtures from the query.
Here is what this query returns:
Current result:
[
{
id: 2,
name: 'Champions League',
fixtures: []
},
{
id: 5,
name: 'Europa League',
fixtures: [
[Object],
[Object],
[Object],
[Object],
]
},
{
id: 8,
name: 'Premier League',
fixtures: []
}
];
And here is what I want to achieve:
Desired result:
[
{
id: 5,
name: 'Europa League',
fixtures: [
[Object],
[Object],
[Object],
[Object],
]
},
];
I am aware that I can do something like leagues.filter(item => item.fixtures.length > 0), but this query is going to be called tens of times each second, I'm afraid that running another filter after the query can lead to performance issues.
Any help or possible alternatives are appreciated.

Categories