Mongoose, virtuals and Schemas - node.js

I'm developing a NodeJS (Typescript) with Mongoose and when I try to add a virtual to my schema in one of the ways Mongoose's documentation suggests, I get an error saying that the prop virtuals doesn't exist in type SchemaOptions. And a correct error, even though the documentation suggests to use it.
This is what I find in the docs:
// That can be done either by adding it to schema options:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
virtuals: { //--------> I get the error here
fullName: {
get() {
return this.name.first + ' ' + this.name.last;
}
}
}
});
This is what I was trying:
const mySchema = new Schema<MyInterface>(
{
someProp: { type: Types.ObjectId, required: true, ref: "some-collection" },
myList: { type: [listSchema], required: true, default: [] },
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
virtuals: {
getByType: {
get: (type: string) => {
return this.myList.filter((item: Item) => item.type === type);
}
}
}
}
);
In the other hand, I can set my virtual this way:
mySchema.virtual("getByType").get((type: string) => {
return this.myList.filter((item: Item) => item.type === type);
});
I had to do a few workarounds to sort the issue about not resolving the this keyword, but so far I have no problem about it...
The problem is: I use findOne and then try to call my virtual get with my document, but I get a Type Error saying Property 'getByType' does not exist on type 'MyInterface & Document<any, any, MyInterface>'.
Looks like there is a mix of mistake on the documentation and a Typescript problem here.
What do you suggest?

Virtual is intended to be used by a Class. If you want to use it on an instance, use methods instead.
Virtual
mySchema.virtual("getByType").get((type: string) => {
return this.myList.filter((item: Item) => item.type === type);
});
// Usage
MySchema.getByType(type)
Methods
mySchema.methods.getByType = function (type, cb) {
return this.model('MySchema').find({
myList: { $elemMatch: { type } }, // Fill your logic here
}, cb)
};
// Usage
const instance = MySchema.findOne();
const result = instance.getByType(type);

Related

Expected 'property' to be of type string, instead found type object - Dynamoose

I am working with AWS DynamoDB and Dynamoose trying to fetch records using Scan function, but facing an issue that is not recognizable for me.
Stragenly, it's able to fetch records from another table in the same way and successfully get the records.
Here's my Code:
const vehicleMasterSchema = new dynamoose.Schema({
"id": String,
"customer_account_number": String,
"fuel_type": String,
"make": String,
"model": String,
"odometer_gatex": String,
"plate_no": String,
"rfid_gatex": String,
"sales_agreement_id": String,
"vehicle_category": String,
"vehicle_id": String,
}, {
"timestamps": {
"createdAt": "create_date",
"updatedAt": null // updatedAt will not be stored as part of the timestamp
}
});
const vehicleMasterModel = dynamoose.model("vehicle_master", vehicleMasterSchema, { "create": false });
router.post('/getFuelingStatus', (req, res) => {
var companyInfo = req.body;
try {
console.log(typeof vehicleMasterModel);
vehicleMasterModel.scan("customer_account_number").eq(companyInfo.customerId).exec((error, results) => {
if (error) {
console.error(error);
} else {
res.json(results);
}
});
} catch (error) {
res.json(error);
}
});
The TypeMismatch error is coming up only for this model same code is working for the other table.
Console Error
My Table
This appears to be related to this github issue on Dyanmoose
My guess is that the problem could be related with the name of your attribute, model.
In fact, this is the actual case: the following code, extracted from the source code in Document.ts is the one which is overwriting your model property:
Object.defineProperty(this, "model", {
"configurable": false,
"value": model
});
This is how the Document looks like before:
And after the execution of the aforementioned code:
This code is executed when processing the Scan exec function in DocumentRetriever.ts when the library maps every Item returned by DynamoDB to their internal Document representation, exactly in this line of code:
const array: any = (await Promise.all(result.Items.map(async (item) => await new this.internalSettings.model.Document(item, {"type": "fromDynamo"}).conformToSchema({"customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "modifiers": ["get"], "type": "fromDynamo"})))).filter((a) => Boolean(a));
The error you reported is a consequence of that change when the type of the returned Item is checked against your schema model in the checkTypeFunction:
const {isValidType, matchedTypeDetails, typeDetailsArray} = utils.dynamoose.getValueTypeCheckResult(schema, value, genericKey, settings, {"standardKey": true, typeIndexOptionMap});
if (!isValidType) {
throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetailsArray.map((detail) => detail.dynamicName ? detail.dynamicName() : detail.name.toLowerCase()).join(", ")}, instead found type ${typeof value}.`);
...
Please, try a different name, I think it will work properly.
Schema must be like this :
const ImageGalleryFoldersSchema = new Schema({
key: {
type: String,
hashKey: true,
required: true,
},
displayName: {
type: String,
required: true,
},
parentFolderKey: {
type: String,
required: false,
},
isActive: {
type: Boolean,
default: true,
required: false,
},
}, {
timestamps: true,
});
Maybe your problem is caused due to asynchronous behaviour.
To be more specific, I think that by the time you call the "scan"-function-chain the body-request has not been finished. However, due to the nature of Hoisting, the object "companyInfo" was already being initialised before you enter the function-call.
Therefore, you may get the specified "TypeMismatch"-error.
Could you please try implementing the following async/await-structure and tell me if this helps:
router.post('/getFuelingStatus', async (req, res) => {
var companyInfo = await req.body;
try {
console.log(typeof vehicleMasterModel);
vehicleMasterModel.scan("customer_account_number").eq(companyInfo.customerId).exec((error, results) => {
if (error) {
console.error(error);
} else {
res.json(results);
}
});
} catch (error) {
res.json(error);
}
});

How to grab field value during a MongooseModel.bulkWrite operation?

Context:
I am trying to upsert in bulk an array of data, with an additional computed field: 'status'.
Status should be either :
- 'New' for newly inserted docs;
- 'Removed' for docs present in DB, but inexistent in incoming dataset;
- a percentage explaining the evolution for the field price, comparing the value in DB to the one in incoming dataset.
Implementations:
data.model.ts
import { Document, model, Model, models, Schema } from 'mongoose';
import { IPertinentData } from './site.model';
const dataSchema: Schema = new Schema({
sourceId: { type: String, required: true },
name: { type: String, required: true },
price: { type: Number, required: true },
reference: { type: String, required: true },
lastModified: { type: Date, required: true },
status: { type: Schema.Types.Mixed, required: true }
});
export interface IData extends IPertinentData, Document {}
export const Data: Model<IData> = models.Data || model<IData>('Data', dataSchema);
data.service.ts
import { Data, IPertinentData } from '../models';
export class DataService {
static async test() {
// await Data.deleteMany({});
const data = [
{
sourceId: 'Y',
reference: `y0`,
name: 'y0',
price: 30
},
{
sourceId: 'Y',
reference: 'y1',
name: 'y1',
price: 30
}
];
return Data.bulkWrite(
data.map(function(d) {
let status = '';
// #ts-ignore
console.log('price', this);
// #ts-ignore
if (!this.price) status = 'New';
// #ts-ignore
else if (this.price !== d.price) {
// #ts-ignore
status = (d.price - this.price) / this.price;
}
return {
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: {
$set: {
// Set percentage value when current price is greater/lower than new price
// Set status to nothing when new and current prices match
status,
name: d.name,
price: d.price
},
$currentDate: {
lastModified: true
}
},
upsert: true
}
};
}
)
);
}
}
... then in my backend controller, i just call it with some route :
try {
const results = await DataService.test();
return new HttpResponseOK(results);
} catch (error) {
return new HttpResponseInternalServerError(error);
}
Problem:
I've tried lot of implementation syntaxes, but all failed either because of type casting, and unsupported syntax like the $ symbol, and restrictions due to the aggregation...
I feel like the above solution might be closest to a working scenario but i'm missing a way to grab the value of the price field BEFORE the actual computation of status and the replacement with updated value.
Here the value of this is undefined while it is supposed to point to current document.
Questions:
Am i using correct Mongoose way for a bulk update ?
if yes, how to get the field value ?
Environment:
NodeJS 13.x
Mongoose 5.8.1
MongoDB 4.2.1
EUREKA !
Finally found a working syntax, pfeeeew...
...
return Data.bulkWrite(
data.map(d => ({
updateOne: {
filter: { sourceId: d.sourceId, reference: d.reference },
update: [
{
$set: {
lastModified: Date.now(),
name: d.name,
status: {
$switch: {
branches: [
// Set status to 'New' for newly inserted docs
{
case: { $eq: [{ $type: '$price' }, 'missing'] },
then: 'New'
},
// Set percentage value when current price is greater/lower than new price
{
case: { $ne: ['$price', d.price] },
then: {
$divide: [{ $subtract: [d.price, '$price'] }, '$price']
}
}
],
// Set status to nothing when new and current prices match
default: ''
}
}
}
},
{
$set: { price: d.price }
}
],
upsert: true
}
}))
);
...
Explanations:
Several problems were blocking me :
the '$field_value_to_check' instead of this.field with undefined 'this' ...
the syntax with $ symbol seems to work only within an aggregation update, using update: [] even if there is only one single $set inside ...
the first condition used for the inserted docs in the upsert process needs to check for the existence of the field price. Only the syntax with BSON $type worked...
Hope it helps other devs in same scenario.

Save array values based on ObjectId using mongoose

I try to save each document in an array as ObjectId, like that:
{
materials: {
active: "Steel",
description: "List of materials",
text: "Materials",
value: ["5c44ea8163bfea185e5e2dfb", "5c44ea8163bfea185e5e2dfc"]
}
}
I used an array of promises to save asynchronous each value and save the callback _id:
const reference = {
materials: {
...project.materials,
value: await Promise.all(project.materials.value.map(
async (value) => {
const { _id } = await Material.findOneAndUpdate({ name: value.name }, value, { upsert: true, new: true, setDefaultsOnInsert: true }).exec();
return mongoose.Types.ObjectId(_id);
}
))
},
...
}
There is another more simple way ?

update array of object of object MongoDB

I have this model
student: {
package:{
type: mongoose.Schema.Types.ObjectId,
ref: 'Package',
},
history: [
{
package: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Package',
},
orderDate: {
type: Date,
default: new Date().toLocaleDateString('id'),
},
Status: {
type: String,
default: 'Pending',
},
},
],
}
What I want to do is that I want to update Student.package and Student.history in 1 pass
I create this method in my model
StudentSchema.methods.updatePackage= function(idPackage) {
this.package = idPackage;
return this.save();
};
StudentSchema.methods.updateHistory= function(idPackage) {
this.history.push(idPackage);
return this.save();
};
and I'm trying to do something like this in my controller
buyPack: async (req, res, next) => {
try {
let dataStudent = await Student.findById('5b83443040e3751bb4e32a21');
await dataStudent.updatePackage(req.body);
await dataStudent.updateHistory(req.body);
return res.json(dataStudent);
} catch (err) {
console.log(err);
next(err);
}
},
I think the first and second methods are wrong, but I have tried to figure it out in almost half of day, but still no luck. What is the best way to achieve my goals?
Do I make my model wrong? or do the methods I created are wrong?
Mongoose model rename with plural .. and you refer with singular with "S", "Package" change it to "Packages"

Mongoose async requests managment

I'm actually trying to convert mongodb references into those references' documents value (info.value) using mongoose in javascript.
Tried that by using map, for/forEach, and nothing did the job since mongoose requests are async.
Not really used to this kind of code, I feel a bit lost after all those things I tried.
Maybe someone would like to give me a hint about this by taking a look at the code below.
Just for information, no need to worry about loading templates, connecting to mongo, ... since everything else is working just fine.
That's the closest I got to the expected result, but still, it throws me errors when I try to "console.log(cond[c]);/console.log(info);" (cond[c] and info are null and undefined)
Well this function also needs to be prepared to be recursive since I plan to put sub-blocks in the "content" property of the bloc objects.
Thanks a lot for your time guys.
// Input condition
"H1Vf3KTef || false"
// Expected result
"1 || false"
// Buggy Function
var execIfBlock = function recursExec (query, callback) {
IfBlockModel.findOne(query, function(err, ifBlock) {
if (!err) {
var cond = ifBlock.condition.split(" ");
//console.log('Block : ' + ifBlock);
//console.log('Condition : ' + cond);
var calls = new Array();
for (var c = 0, len = cond.length; c < len; c++) {
if (shortId.isValid(cond[c])) {
calls.push(function() {
InfoModel.findOne({ _id: cond[c] }, function(err, info) {
console.log(cond[c]);
console.log(info);
cond[c] = info.value;
});
});
}
}
async.parallel(calls, function(err, result) {
console.log(result);
// Do some job using the final expected result : "1 || false"
});
}
});
};
// Info template
{
"_id": "H1Vf3KTef",
"value": "1"
}
// Bloc template
{
"_id": "rkRBtLTef",
"content": [],
"condition": "H1Vf3KTef || false"
}
// Info schema
var InfoSchema = new Schema({
_id: { type: String, unique: true, required: true, default: shortId.generate },
value: { type: String, default: "0" }
});
// Bloc schema
var IfBlockSchema = new Schema({
_id: { type: String, unique: true, required: true, default: shortId.generate },
condition: { type: String, required: true, default: true },
content: [{ type: String, required: true, default: '', ref: 'block' }]
});
Use promises and break your code in small functions :
var execIfBlock = function recursExec(query, callback) {
IfBlockModel.findOne(query, function (err, ifBlock) {
if (!err) {
var cond = ifBlock.condition.split(" ");
updateMultipeInfo(cond)
.then(values => {
console.log(values) // [values1, values ,...]
});
}
});
};
function updateMultipeInfo(cond){
return Promise.all(cond.map(updateInfo))
}
function updateInfo(id){
if (shortId.isValid(id)) {
return InfoModel
.findOne({ _id: id })
.then(info => info.value);
} else {
return Promise.reject("invalid id");
}
}

Resources