How to search in a REST API express with multiple fields - javascript

I would like to perform a search request like https://api.mywebsite.com/users/search?firstname=jo&lastname=smit&date_of_birth=1980
I have a User schema like:
var UserSchema = new mongoose.Schema({
role: { type: String, default: 'user' },
firstname: { type: String, default: null },
lastname: { type: String, default: null },
date_of_birth: { type: Date, default: null, select: false },
});
What I did so far with stackoverflow help:
// check every element in the query and perform check function
function search_t(query) {
return function (element) {
for (var i in query) {
if (query[i].function(element[i], query[i].value) == false) {
return false;
}
}
return true;
}
}
// prepare query object, convert elements and add check function
// convert functions are used to convert string (from the query) in the right format
// check functions are used to check values from our users
function prepareSearch(query, cb) {
let fields = {
"firstname": {
"type": "string",
"function": checkString,
"convert": convertString
},
"lastname": {
"type": "string",
"function": checkString,
"convert": convertString
},
"date_of_birth": {
"type": "date",
"function": checkDate,
"convert": convertDate
}
};
for (let k in query) {
k = k.toLowerCase();
if (!(k in fields)) {
return cb({message: "error"});
}
query[k] = {value: fields[k].convert(query[k]), function: fields[k].function};
}
return cb(null, query);
}
// linked to a route like router.get('/search/', controller.search);
export function search(req, res) {
return User.find({}).exec()
.then(users => {
return prepareSearch(req.query, (err, query) => {
if (err) {
return handleError(res)(err);
} else {
return res.status(200).send(users.filter(search_t(query)));
}
});
})
.catch(handleError(res));
}
So this code works but I don't know if it's a good thing. I have a lot of other fields to check (like gender, ....) and I don't know if it's a good thing to do it "manually".
I don't know if mongoose has any function to do it.
Should I use another method to filter / search in my users in my REST API ?
I'm pretty new here and I am not sure about how I work...
Thank you,
Ankirama

Related

Mongodb - Update property per object in array of object, Insert if doesn't exists

I wish to update a property per object in array of objects, but if some of the objects doesn't exists, insert the object instead.
Currently I used "upsert", which creates a new document when no document matches the query, unfortunately it is replacing a single item with my entire list.
Worth to mention that I am using mongoist to perform an async requests.
My code:
this.tokenArray = [
{ token: "654364543" },
{ token: "765478656" },
{ token: "876584432" },
{ token: "125452346" },
{ token: "874698557" },
{ token: "654364543" }
]
database.upsertDatebaseItem(this.tokenArray.map(x => { return x.token }), { valid : true }, 'Tokens');
async upsertDatebaseItem(itemKey, itemValue, collectionName) {
try {
await this.database[collectionName].update({ token : { $in: itemKey}}, { $set: itemValue }, {upsert : true} , {multi : true});
} catch (error) {
console.log(`An error occurred while attempting to update ${itemType} to the database: ${error}`);
return false;
}
}
Found the way to do it:
const bulkUpdate = this.tokenArray.map((x) => {
return {
"updateOne": {
"filter": { "token": x.token },
"update": { "$set": { "valid": true } },
"upsert": true
}
};
});
and:
this.database[collectionName].bulkWrite(bulkUpdate);
To upsert with mongoist, use the following:
var bulk = db.collection.initializeOrderedBulkOp()
for(var doc of docs) bulk.find( { _id: doc._id } ).upsert().updateOne(doc); // or use replaceOne()
await bulk.execute();
Converted to your case that would be
var bulk = db.collectionName.initializeOrderedBulkOp()
for(var tokenItem of tokenArray) bulk.find( { token : tokenItem.token } ).upsert().updateOne(tokenItem); // or use replaceOne()
await bulk.execute();

Node js Unit testing method where fields of a model are not selected by default

Node js with mongoose.
model has fields that are not selected by default queries(select:false in model). In some scenarios I need to test the result to have the field selected. I have tried sinon-mongoose but it just tells me if a populate or select method was called, but for more flexibility I need to check the result data. And a requirement is not to use any real db connection, as it is a unit test.
The model
var DoctorSchema = new Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true
},
middleName: {
type: String
},
institutionName: {
type: String,
required: true,
select: false
}
});
The Service
module.exports = function (Doctor) {
service.getById = function(req, res, next){
Doctor.findOne({"_id" : req.params.id}).select('+institutionName').exec(function (err, doc) {
if (err) {
console.log(err);
return
};
if (!doc) {
res.send(404);
}
else {
res.json(doc);
}
});
}
}
So this can be tested
describe('doctorService getById', function () {
it('presentationService should call getById', function (done) {
var DoctorMock = sinon.mock(DoctorModel);
DoctorMock
.expects('findOne').withArgs({ "_id": "123" })
.chain("select").withArgs('+institutionName')
.chain('exec')
.yields(null, seed.doctor);
var serviceMock = doctorService(DoctorModel);
var res = {
json: function (data) {
DoctorMock.verify();
DoctorMock.restore();
assert.equal(seed.doctor, data, "Test fails due to unexpected result");
done();
}
};
serviceMock.getById({param:{id:"123"}}, res);
});
});
But in my example the test is bound to the chain. My intuition tells me that this is not a good approach.

Updating info with Mongoose, array inside object inside object dynamically

I'm trying to update the JSON field "champ_x" from 1 to 3 and for both players 1 at a time in a dynamic function:
{
"_id": {
"$oid": "58a3521edf127d0a0c417cda"
},
"room": "room_0.0940045412694186",
"player_1": "eee",
"player_2": "fff",
"player_1_details": {
"history_moves": [],
"champions": [
{
"champ_1": "na"
},
{
"champ_2": "na"
},
{
"champ_3": "na"
}
]
},
"player_2_details": {
"history_moves": [],
"champions": [
{
"champ_1": "na"
},
{
"champ_2": "na"
},
{
"champ_3": "na"
}
]
},
"game_state": "789",
"__v": 0
}
I've got this model:
match_schema.statics.update_champ = function(room, turn, champ_num, champ_select, callback){
if(champ_num == "champ_1"){
match_mongoose.update({ room: room }, { $set: { 'player_1_details.champions.0.champ_1': champ_select}})
.exec(function(error){
if(error){ return callback(error); }else{ return callback(null); }
});
}
};
This model works fine
My problem is, I'm trying to make it dynamic, in which I can just send through the function parameters the current turn(1 or 2), and the chosen position(champ_1,2, or 3).
I've tried this:
//Update Champion
match_schema.statics.update_champ = function(room, turn, champ_num, champ_select, callback){
match_mongoose.update({ room: room }, { $set: { 'player_'+turn+'_details.champions.0.'+champ_num: champ_select}})
.exec(function(error){
if(error){ return callback(error); }else{ return callback(null); }
});
};
var match_mongoose = mongoose.model('matches', match_schema, 'matches');
module.exports = match_mongoose;
But I get an error that says the "Unexpected token +" seems like concatenating the value doesn't work. Is there a way to do this?
Thanks!
You may build the $set modifier and the match part as suggested by #dNitro :
var modifier = { $set: {} };
modifier.$set['player_' + turn + '_details.champions.$.champ_' + champ_num] = champ_select;
You will have also an issue with array index, you specify champions.0 so it will always take the first array item which won't match for champs_2 & champs_3. One solution for this is to use positional parameter $ with a match from the array :
var match = {};
match['room'] = room;
match['player_' + turn + '_details.champions.champ_' + champ_num] = { $exists: true };
The full update function is :
matchSchema.statics.update_champ = function(room, turn, champ_num, champ_select, callback) {
var modifier = { $set: {} };
modifier.$set['player_' + turn + '_details.champions.$.champ_' + champ_num] = champ_select;
var match = {};
match['room'] = room;
match['player_' + turn + '_details.champions.champ_' + champ_num] = { $exists: true };
this.update(match, modifier)
.exec(function(error) {
if (error) {
return callback(error);
} else {
return callback(null);
}
});
};
And calling it with :
Match.update_champ("room_0.0940045412694186", 1, 1, "new_value", function(err, res) {
if (!err) {
console.log(err);
return;
}
console.log(res);
});
You can find a full example here

How is it possible for a string to be typed as Number in npm package commandLineArgs

In the below code from MongoDB's course Week 3's Query Operators in the Node.js Driver chapter :
var MongoClient = require('mongodb').MongoClient,
commandLineArgs = require('command-line-args'),
assert = require('assert');
var options = commandLineOptions();
MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) {
assert.equal(err, null);
console.log("Successfully connected to MongoDB.");
var query = queryDocument(options);
var projection = {
"_id": 1,
"name": 1,
"founded_year": 1,
"number_of_employees": 1,
"crunchbase_url": 1
};
var cursor = db.collection('companies').find(query, projection);
var numMatches = 0;
cursor.forEach(
function(doc) {
numMatches = numMatches + 1;
console.log(doc);
},
function(err) {
assert.equal(err, null);
console.log("Our query was:" + JSON.stringify(query));
console.log("Matching documents: " + numMatches);
return db.close();
}
);
});
function queryDocument(options) {
console.log(options);
var query = {
"founded_year": {
"$gte": options.firstYear,
"$lte": options.lastYear
}
};
if ("employees" in options) {
query.number_of_employees = {
"$gte": options.employees
};
}
return query;
}
function commandLineOptions() {
var cli = commandLineArgs([{
name: "firstYear",
alias: "f",
type: Number
}, {
name: "lastYear",
alias: "l",
type: Number
}, {
name: "employees",
alias: "e",
type: Number
}]);
var options = cli.parse()
if (!(("firstYear" in options) && ("lastYear" in options))) {
console.log(cli.getUsage({
title: "Usage",
description: "The first two options below are required. The rest are optional."
}));
process.exit();
}
return options;
}
I'm requiring command-line-args package, which has a method commandLineArgs. All good and fine...
Now, I see that the type of the objects passed to this method is set to Number. We can clearly see that they're Strings.
How is it possible?
From the command-line-args GitHub page:
The type value is a setter function (you receive the output from this), enabling you to be specific about the type and value received.
In other words, passing Number as type allows you to parse the arguments as numbers.

What should go in the model and what should go in the controller?

First I'm doing this in NoSQL & node.js. I assume that shouldn't effect the question but it will help understand my example.
I have a model for a page and a tag that look something like this:
var Page = app.ormDb.define('pages', {
uuid : { type: String, index: true, length: 40, default: function () { return uuid.v4(); } },
title : { type: String, index: true, length: 255 },
url : { type: String, index: true, length: 255 },
summary_mu : { type: String, length: 1024 },
summary_html : { type: String },
summary_hist : { type: JSON, default: function() { rerutn { items : [] }; } },
sections : { type: JSON, default: function() { rerutn { items : [] }; } },
tags : { type: JSON, default: function() { rerutn { items : [] }; } },
date : { type: Date, default: function() { return new Date(); } },
updated : { type: Date, default: function() { return new Date(); } }
});
var Tag = app.ormDb.define('tags', {
uuid : { type: String, index: true, length: 40, default: function () { return uuid.v4(); } },
name : { type: String, index: true, length: 255 },
url : { type: String, index: true, length: 255 },
desc : { type: String, length: 1024 },
date : { type: Date, default: function() { return new Date(); } },
updated : { type: Date, default: function() { return new Date(); } }
});
So now I have some data maintenance issues, for example when I add a tag to a page I need to make sure there is a tag entry. To this end I've created a method on the model.
// Add a tag to a page
// tag .. The tag to add
Page.prototype.addTag(tag, done) {
var _this = this;
if (_this.tags == null) {
_this.tags = { items:[] };
}
var index = _this.tags.items.indexOf(tag);
if (index == -1) {
_this.tags.items.push(tag);
}
async.waterfall([
function (cb) {
app.models.tag.count({'name': tag}, cb);
},
function (count, cb) {
if (count == 0) {
app.models.tag.create({
name : tag,
}, function (err, newTag) {
return cb(err, tag);
});
} else {
return cb(null, tag);
}
}
], function (err, items) {
done(err, items);
});
}
In the controller I have code that verifies the user input, loads the current page, calls the Page method above to add the tag, and finally saving the updated page. Note, in the method above I'm checking the Tag collection for the existence of the tag, and creating it if needed.
Is this correct, or should the Page method's logic be moved to the controller and the model only deal with the one model and not others?
My reasoning is that I would never add a tag to a page without checking/creating the tag in the Tag collection.

Categories