Defining mongoose Model Methods Dynamically - javascript

I'm in the case of a function being able to search by different fields depending on the situation.
It returns the same dataset, it just searches by different fields: either userId or tagId. Therefore, in my code I have something like this:
var findByMethod;
if (searchBy === 'userId') {
findByMethod = UserArticleModel.findByUser;
}
else {
findByMethod = UserArticleModel.findByTag;
}
findByMethod(idToSearch, function (err, articles) {…});
findByUser and findByTag are static methods defined in the UserArticleModel.js
UserArticleModel.js
var mongoose = require('mongoose');
var userArticleSchema = new mongoose.Schema({
…
}
});
userArticleSchema.statics.findByUser = function (userId, callback) {
this.find({userId: userId}, function () {…});
};
userArticleSchema.statics.findByTag = function (tagId, callback) {…};
module.exports = mongoose.model('UserArticle', userArticleSchema);
Back in my controller, when I do:
UserArticleModel.findByTag(idToSearch, function (err, articles) {…});
All is well and things go right. But when I dynamically call the method via my variable:
findByMethod(idToSearch, function (err, articles) {…});
Things go wrong as node returns an error:
DOMAINE ERROR CAUGHT: TypeError: Object #<Object> has no method 'find'
I suspect this not to be be bound to the correct scope but I don't really understand why as findByMethod === UserArticleModel.findByUser // true

I think you are making this more involved that it need be. Though it is an easy trap to fall into by "too literally" following documented API examples and thinking essentially "this is how I need to hardcode this, because the docs say this is how you do it".
JavaScript objects are, well "Objects", and therefore just assigning "named" static methods which are really only object properties is just a basic process of "looping" the defined "schema paths" from that already defined "Object" and setting up the properties for the "findByFieldName" methods you want.
It's just "assigning named properties" and nothing more obscure or complex or even as "terse" than that.
If that "sounds like a mouthful" then the actual process of iterating object properties and "setting other properties" related to that within an overall object structure is not really as hard as you might think.
As a brief example:
var async = require('async'),
pascal = require('to-pascal-case'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var testSchema = new Schema({
fieldA: String,
fieldB: Number
});
function setStatics(schema) {
Object.keys(schema.paths).filter(function(key) {
return key != '_id';
}).forEach(function(key) {
schema.statics['findBy' + pascal(key)] = function(arg,callback) {
var query = {};
query[key] = arg;
this.findOne(query,callback);
};
});
};
// Set up findByFieldName other than _id
setStatics(testSchema);
var Test = mongoose.model( 'Test', testSchema, "test" );
mongoose.connect('mongodb://localhost/test');
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
Test.create([
{ "fieldA": "a", "fieldB": 1 },
{ "fieldA": "b", "fieldB": 2 }
],callback);
},
function(callback) {
Test.findByFieldA("a",function(err,doc) {
console.log(doc);
callback(err);
});
},
function(callback) {
Test.findByFieldB(2,function(err,doc) {
console.log(doc);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
Which proves that they work by "testing them" with the output:
{ _id: 55f2ae1b7d8315f40b1a2b77, fieldA: 'a', fieldB: 1, __v: 0 }
{ _id: 55f2ae1b7d8315f40b1a2b78, fieldA: 'b', fieldB: 2, __v: 0 }
And that is all there is to it.
Of course for fields like "Arrays" you want to get a little more involved, but this is the basic premise as a listing you can try out yourself ( or selves for the community in general ).
I could also note that there are already a few things out there such as Bluebird via it's own .promisifyAll() call which interacts with objects to set new up "named methods" on the object in a similar way. Or at least it should be similar in principle, as I have not actually looked at that code.

Related

How to use equals in a FilterExpression for DyanmoDb API

I am having a lot of trouble scanning and then using FilterExpression to filter based on a single value. I have looked at the api documentation and other stack overflow questions, but am still having trouble figuring the proper syntax for this. Since I am also using react and javascript for the first time, this may be a problem with my understanding of those.
Below is what I am trying to use as a filter expression. uploadId is the field name in the Dynamo database table and event.pathParameters.id is the variable that should resolve to the value that the scan results are filtered on.
FilterExpression: "uploadId = :event.pathParameters.id"
Below is the code in within context:
import * as dynamoDbLib from "./libs/dynamodb-lib";
import { success, failure } from "./libs/response-lib";
export async function main(event, context, callback) {
const params = {
TableName: "uploads",
FilterExpression: "uploadId = :event.pathParameters.id"
};
try {
const result = await dynamoDbLib.call("scan", params);
if (result.Item) {
// Return the retrieved item
callback(null, success(result.Item));
} else {
callback(null, failure({ status: false, error: "Item not found." }));
}
} catch (e) {
callback(null, failure({ status: false }));
}
}
Thank you for your help!
Always use Expression with ExpressionAttributeValues. params should look like this.
const params = {
TableName: "uploads",
FilterExpression: "uploadId = :uid",
ExpressionAttributeValues: {
":uid" : {S: event.pathParameters.id} //DynamoDB Attribute Value structure. S refer to String, N refer to Number, etc..
}
};

Angular / Typescript Cannot set property 'data' of undefined

I have some code which is reading xml then parsing the data.
So here's the code:
data = [];
ngOnInit() {
this.myService.getData().subscribe(XMLResult => {
const parseString = require('xml2js').parseString;
parseString(XMLResult, function (err, result) {
console.dir(result.listResponse.instance); // Returns Array
this.data = result.listResponse.instance; // Here's the line giving me the error
});
});
}
Console.log is returning the data like this:
Array(10)
0:
$: {id: "23" name: "name 1"}
1:
$: {id: "45" name: "name 2"}
etc
How can I fix this problem?
In your current approach, this does not refer to your component anymore.
Modify your callback to use an arrow function, so that you don't lose the scope:
parseString(XMLResult, (err, result) => {
console.dir(result.listResponse.instance);
this.data = result.listResponse.instance;
})
Docs
Until arrow functions, every new function defined its own this value
(a new object in the case of a constructor, undefined in strict mode
function calls, the base object if the function is called as an
"object method", etc.). This proved to be less than ideal with an
object-oriented style of programming.

Mongoose infinite loop on Model creation

Background
I have Mongoose Schema about Surveys, that needs to check if the Survey belongs to a set of countries that is in another collection.
Code
To check this, I have a surveySchema, a countrySchema, and a file where I create the models and connect to the DB.
To perform the check that a survey belongs to a valid country, I am using Mongoose async validators in surveySchema like the following:
surveySchema.js:
"use strict";
const mongoose = require("mongoose");
const surveySchema = {
subject: { type: String, required: true },
country: {
type: String,
validate: {
validator: {
isAsync: true,
validator: async function(val, callback) {
const {
Country
} = require("./models.js").getModels();
const countriesNum = await Country.find({"isoCodes.alpha2": val}).count();
callback(countriesNum === 1);
}
},
message: "The country {VALUE} is not available in the DB at the moment."
}
}
};
module.exports = new mongoose.Schema(surveySchema);
module.exports.surveySchema = surveySchema;
countrySchema.js:
"use strict";
const mongoose = require("mongoose");
const countrySchema = {
name: { type: String, required: true },
isoCodes:{
alpha2: { type: String, required: true }
}
}
};
module.exports = new mongoose.Schema(countrySchema);
module.exports.countrySchema = countrySchema;
models.js:
"use strict";
const mongoose = require("mongoose");
const fs = require("fs");
const DB_CONFIG = "./config/dbConfig.json";
/**
* Module responsible for initializing the Models. Should be a Singleton.
*/
module.exports = (function() {
let models;
const initialize = () => {
//Connect to DB
const {
dbConnectionURL
} = JSON.parse(fs.readFileSync(DB_CONFIG, "utf8"));
mongoose.connect(dbConnectionURL);
mongoose.Promise = global.Promise;
//Build Models Object
models = {
Country: mongoose.model('Country', require("./countrySchema.js")),
Survey: mongoose.model('Survey', require("./surveySchema.js"))
};
};
const getModels = () => {
if (models === undefined)
initialize();
return models;
};
return Object.freeze({
getModels
});
}());
The idea here is that I am using the models.js file in other places as well. Because this file is also responsible for connecting to the DB, I decided to make it a Singleton. This way, I should only connect once, and all further requests will always return the same Models, which would be ideal.
Problem
The problem here is that I have a circular dependency that results in:
RangeError: Maximum call stack size exceeded at exports.isMongooseObject (/home/ubuntu/workspace/server/node_modules/mongoose/lib/utils.js:537:12)
...
The flow of code leading to this error is:
Code runs getModels()`
getModels() checks that models is undefined and runs initialize()
initialize() tries to create the models.
When creating the Survey model Survey: mongoose.model('Survey', require("./surveySchema.js")) it runs into the validator function, which again requires models.js
Infinite loop begins
Questions
Is there any other way to check if a Survey's country is part of the county's collection without making a async validation?
How can I structure/change my code so this doesn't happen?
As said in the comments, I think you are a bit confused about how you are using your models.js module. I think this is what is happening:
You are exporting a single function from models.js:
models.js
module.exports = function() { ... };
Therefore, when you require it, you just get that single function:
surveySchema.js
const models = require("./models.js");
models is now a function. Which means every time you call it, you run through the code in models.js and create a new variable let models;, and also new functions initialize() and getModels().
You could move the let models out of the anonymous function into the global scope which would probably fix it, but for my money you only want to run the anonymous function in models.js once, so I would invoke it immediately and set the exports of the module to its result:
models.js
module.exports = (function() {
// codez here
return Object.freeze({
getModels
});
})(); // immediately invoke the function.
Use it:
// models is now the frozen object returned
const { Survey } = models.getModels();
As for options to validation, there's no reason why you can't add your own middleware validation code if normal async validation doesn't do it for you using serial or parallel mechanisms as described in the docs.
Update after comments
The second problem as you point out is that during first execution of getModels() -> initialize() you call require('./surveySchema.js'), but this calls getModels() which is still in the process of being called and hasn't yet initialized models, so initialize() is re-entered.
I think what you're trying to achieve is fine (survey schema depends on customer model), because you can still draw an object graph without any circular dependencies, and it's just the way you've implemented it that you've ended up with one. The simplest way to deal with this I think is actually to keep the circular reference, but defer the point at which you call getModels() in surveySchema.js:
"use strict";
const mongoose = require("mongoose");
const models = require("./models.js");
const surveySchema = {
subject: { type: String, required: true },
country: {
type: String,
validate: {
validator: {
isAsync: true,
validator: async function(val, callback) {
// !! moved from the global scope to here, where we actually use it
const {
Country
} = models.getModels();
const countries = await Country.find({"isoCodes.alpha2": val});
callback(countries.length === 1);
}
},
message: "The country {VALUE} is not available in the DB at the moment."
}
}
};
module.exports = new mongoose.Schema(surveySchema);
module.exports.surveySchema = surveySchema;
A neater and probably more extensible way of approaching it, though, might be to separate out the connection code from the models code, since it's a different concept altogether.
Update #2 after more comments
The infinite stack you're seeing is because you have not used the API correctly. You have:
const surveySchema = {
country: {
validate: {
validator: {
isAsync: true,
validator: async function(val, callback) {...}
},
},
...
}
};
You should have:
const surveySchema = {
country: {
validate: {
isAsync: true,
validator: async function(val, callback) {...}
},
...
}
};
As per the docs.

mongoose save vs insert vs create

What are different ways to insert a document(record) into MongoDB using Mongoose?
My current attempt:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var notificationsSchema = mongoose.Schema({
"datetime" : {
type: Date,
default: Date.now
},
"ownerId":{
type:String
},
"customerId" : {
type:String
},
"title" : {
type:String
},
"message" : {
type:String
}
});
var notifications = module.exports = mongoose.model('notifications', notificationsSchema);
module.exports.saveNotification = function(notificationObj, callback){
//notifications.insert(notificationObj); won't work
//notifications.save(notificationObj); won't work
notifications.create(notificationObj); //work but created duplicated document
}
Any idea why insert and save doesn't work in my case? I tried create, it inserted 2 document instead of 1. That's strange.
The .save() is an instance method of the model, while the .create() is called directly from the Model as a method call, being static in nature, and takes the object as a first parameter.
var mongoose = require('mongoose');
var notificationSchema = mongoose.Schema({
"datetime" : {
type: Date,
default: Date.now
},
"ownerId":{
type:String
},
"customerId" : {
type:String
},
"title" : {
type:String
},
"message" : {
type:String
}
});
var Notification = mongoose.model('Notification', notificationsSchema);
function saveNotification1(data) {
var notification = new Notification(data);
notification.save(function (err) {
if (err) return handleError(err);
// saved!
})
}
function saveNotification2(data) {
Notification.create(data, function (err, small) {
if (err) return handleError(err);
// saved!
})
}
Export whatever functions you would want outside.
More at the Mongoose Docs, or consider reading the reference of the Model prototype in Mongoose.
You can either use save() or create().
save() can only be used on a new document of the model while create() can be used on the model. Below, I have given a simple example.
Tour Model
const mongoose = require("mongoose");
const tourSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "A tour must have a name"],
unique: true,
},
rating: {
type: Number,
default:3.0,
},
price: {
type: Number,
required: [true, "A tour must have a price"],
},
});
const Tour = mongoose.model("Tour", tourSchema);
module.exports = Tour;
Tour Controller
const Tour = require('../models/tourModel');
exports.createTour = async (req, res) => {
// method 1
const newTour = await Tour.create(req.body);
// method 2
const newTour = new Tour(req.body);
await newTour.save();
}
Make sure to use either Method 1 or Method 2.
I'm quoting Mongoose's Constructing Documents documentation:
const Tank = mongoose.model('Tank', yourSchema);
const small = new Tank({ size: 'small' });
small.save(function (err) {
if (err) return handleError(err);
// saved!
});
// or
Tank.create({ size: 'small' }, function (err, small) {
if (err) return handleError(err);
// saved!
});
// or, for inserting large batches of documents
Tank.insertMany([{ size: 'small' }], function(err) {
});
TLDR: Use Create (save is expert-mode)
The main difference between using the create and save methods in Mongoose is that create is a convenience method that automatically calls new Model() and save() for you, while save is a method that is called on a Mongoose document instance.
When you call the create method on a Mongoose model, it creates a new instance of the model, sets the properties, and then saves the document to the database. This method is useful when you want to create a new document and insert it into the database in one step. This makes the creation an atomic transaction. Therefore, the save method leaves the potential to create inefficiencies/inconsistencies in your code.
On the other hand, the save method is called on an instance of a Mongoose document, after you have made changes to it. This method will validate the document and save the changes to the database.
Another difference is that create method can insert multiple documents at once, by passing an array of documents as parameter, while save is intended to be used on a single document.
So, if you want to create a new instance of a model and save it to the database in one step, you can use the create method. If you have an existing instance of a model that you want to save to the database, you should use the save method.
Also, if you have any validation or pre-save hook in your content schema, this will be triggered when using the create method.

How to properly pass data from mongodb using callback when looping through multiple db.Phrase.find calls

I'm receiving params from my get request that looks like this:
{ location: 'Venice', weather: 'Dry', what: 'Yoga', who: 'Bob' }
I then query a mongodb database that loops through each of the key and value pairs and queries for their union in the database.
I then save the returned values to outputCaption and then use a callback to pass the outputCaption back.
The problem is the callback gets called as many times as their key-value pairs looped over.
I'm forced to do this because I need the callback inside the db.Phrase.find call but I call that multiple times...
So I've fixed it using the code in app.get (I wait until all the keys have defined values in outputCaption before doing anything)
It works, but I can't imagine it's the best way to do it so I'm hoping there's a less hackish way?
Thanks
server.js
var express = require('express');
var db = require('./modules/db')
var logic = require('./modules/logic')
...
app.get('/phrase', function(req, res){
logic(req.query, function(outputCaption){
var flag = true
for (key in outputCaption){
if (outputCaption[key] === null){
console.log('Incomplete')
var flag = false;
}
}
if (flag === true) {
console.log(outputCaption);
};
});
})
...
logic.js
var db = require('./db')
var logic = function(params, callback){
var outputCaption = {
who: null,
what: null,
location: null,
weather: null
};
for (key in params){
category = key.toLowerCase();
option = params[key].toLowerCase();
db.Phrase.find({$and: [
{category: category},
{option: option}
]}, function(err, phrases){
if (err) return console.error(err);
var options = Object.keys(phrases).length
var idxOfOptionPicked = Math.floor(Math.random() * options)
outputCaption[phrases[idxOfOptionPicked].category] = phrases[idxOfOptionPicked].phrase
callback(outputCaption)
})
}
}
module.exports = logic;
Instead of firing multiple queries and performing a union of the result at the client side, make use of the query operators to allow MongoDB to do the union for you.
That way you:
Avoid multiple hits to the database.
Avoid multiple callback handlers.
Can post process the results in a single callback handler.
You can modify your code to prepare a query object from the request parameter,
var params = {location: 'Venice', weather: 'Dry', what: 'Yoga', who: 'Bob' };
var query = {};
var conditions = [];
Object.keys(params).forEach(function(key){
var $and = {};
$and["category"] = key.toLowerCase();
$and["option"] = params[key];
conditions.push($and);
});
(conditions.length > 1)?(query["$or"] = conditions):(query = conditions[0]);
Now the constructed query looks like:
{ '$or':
[ { category: 'location', option: 'Venice' },
{ category: 'weather', option: 'Dry' },
{ category: 'what', option: 'Yoga' },
{ category: 'who', option: 'Bob' }
]
}
You can pass this object to the find() method, to get the results in a single hit:
db.Phrase.find(query,callback);
This way your code remains cleaner and easier to understand.

Categories