How to make a search api in nodejs and mongodb - javascript

I am trying to make a search api using nodejs and MongoDB. I tried to google about this and I did find something there but while trying to implement I get an error saying. I don't know how to fix this honestly I don't know anything about making search API. So any help or suggestion will be helpful for me.
This is the link of the post I found on google Building a simple search api.
error
{
"error": {
"message": "Cast to ObjectId failed for value \"search\" at path \"_id\" for model \"Post\"",
"name": "CastError",
"stringValue": "\"search\"",
"kind": "ObjectId",
"value": "search",
"path": "_id"
}
}
This is my code
postController.search = (req, res) => {
var response = [];
if(typeof req.query.title !== 'undefined'){
db.Post.filter(function(post) {
if(post.title === req.query.title){
console.log(req.body);
response.push(post);
console.log(post);
}
});
}
response = _.uniqBy(response, '_id');
if(Object.key(req.query).length === 0){
response = db.Post
}
res.json(response);
};
data in the collection
"data": [
{
"isDeleted": false,
"_comments": [],
"_id": "5d39122036117d2ea81b434c",
"title": "facebook post",
"link": "facebook.com",
"_creator": {
"createdAt": "2019-07-25T01:42:21.252Z",
"username": "adityakmr"
},
"createdAt": "2019-07-25T02:21:20.634Z",
"__v": 0
},
]

If you're trying to create an API to search mongoDB collection based on title i.e; a text field try implementing text search feature of mongoDB : text search in mongoDB
, Just create a text index on title field & then create an API with post method which takes in parameter that can be queried against title field.
Text search can be a bit tricky it can help you for fuzzy/partial/full text searches - use of regex is also much beneficial.
Checkout links for node.js API example :
MongoDB NodeJs Docs
Full Text Search with MongoDB & Node.js
Text Searching with MongoDB

First of all, you need to use async/await for modularize your code. I suggest don't write your whole code in your controller.js file, API can be made by following the way (routes - controller - utils ).
postRoutes.js
postRouter.get('/search-post', postCtr.searchPost);
postController.js
const postUtils = require('./postUtils');
const postController = {};
postController.searchPost = async (req, res) => {
try {
const { title } = req.query;
const result = await postUtils.searchPost(title);
return res.status(200).json(result);
} catch (err) {
return res.status(err.code).json({ error: err.error });
}
};
module.exports = postController;
postUtils.js
const Post = require('./postModel');
const postUtils = {};
postUtils.searchPost = async (title) => {
try {
let result = [];
if(title){
// Even you can perform regex in your search
result = await Post.find({ title: title });
}
return result;
} catch (err) {
const errorObj = { code: 500, error: 'Internal server error' }; // It can be dynamic
throw errorObj;
}
};
module.exports = postUtils;
postModel.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user',
required: true,
},
// Your fields ...
}, { collection: 'post', timestamps: true });
const post = mongoose.model('post', postSchema);
module.exports = post;
Using this structure you can easily debug your code and It's also manageable.

In the link you specified above, they are using array of objects stored in file called store.js, but not mongoDB. So directly they are filtering using Array.filter method.
But in mongoDB using mongoose(object modeling tool) you can make use of collection.find() method.
So solution to your problem is as follows
postController.search = async (req, res) => {
var response = [];
if (req.query.title) {
response = await db.Post.find({title: req.query.title});
}
res.json(response);
};
find is inbuilt query method which helps in querying the collections, you can pass multiple properties for querying.

Related

Rename database field within array in MongoDB

I need to change a few database fields in my backend controller before returning the object to the frontend.
Currently, I am doing it in the front end like this with my returned object:
for (let contribution of contributions) {
contribution["title"] = "You Added One"
contribution["launchName"] = contribution.name
contribution["launchId"] = contribution._id
contribution["date"] = contribution.addedAt
contribution["content"] = contribution.description
}
But I am now trying to do this work in the backend using Mongo.
This is my controller:
const Launch = require('../models/launch')
const User = require('../models/user')
async function getRecentActivityByUserId (req, res) {
const { userId } = req.params
const user = await User.findOne({ userId }).lean() || []
const contributions = await Launch.find({ _id: { $in: user.contributions } })
return res.status(200).send(contributions.reverse())
}
So this correctly returns an object to the frontend but I still need to change the database field names.
So I tried this:
async function getRecentActivityByUserId (req, res) {
let recents = []
const { userId } = req.params
const user = await User.findOne({ userId }).lean() || []
const contributions = await Launch.find({ _id: { $in: user.contributions } }).aggregate([
{
$addFields: {
plans: {
$map:{
input: "$launch",
as: "l",
in: {
title: "You Added One",
launchName: "$$l.name",
launchId: "$$l._id",
date: "$$l.addedAt",
content: "$$l.description",
}
}
}
}
},
{
$out: "launch"
}
])
return res.status(200).send(contributions.reverse())
}
The above throws an error saying that I .aggregrate is not a function on .find. Even if I remove the .find, the object returned is just an empty array so I'm obviously not aggregating correctly.
How can I combine .find with .aggregate and what is wrong with my .aggregate function??
I also tried combining aggregate with find like this and get the error Arguments must be aggregate pipeline operators:
const contributions = await Launch.aggregate([
{
$match: {
_id: { $in: user.contributions }
},
$addFields: {
plans: {
$map:{
input: "$launch",
as: "l",
in: {
title: "You Added a Kayak Launch",
launchName: "$$l.name",
launchId: "$$l._id",
date: "$$l.addedAt",
content: "$$l.description",
}
}
}
}
},
{
$out: "launch"
}
])
EDIT: Just realized that I have the word plans in the aggregate function and that is not relevant to my code. I copied this code from elsewhere so not sure what the value should be.
I figured it out. This is the solution:
async function getRecentActivityByUserId (req, res) {
let recents = []
const { userId } = req.params
const user = await User.findOne({ userId }).lean() || []
const contributions = await Launch.aggregate([
{
$match: {
_id: { $in: user.contributions }
}
},
{
$addFields: {
title: "You Added One" ,
launchName: "$name",
launchId: "$_id",
date: "$addedAt",
content: "$description"
}
}
])
if(contributions) {
recents = recents.concat(contributions);
}
return res.status(200).send(recents.reverse())
}
The actual problem from the question was a small syntax error which has been noted and corrected in the self-reported answer here.
I noted in the comments there that the current approach of issuing two separate operations (a findOne() followed by an aggregate() that uses the results) could be simplified into a single query to the database. The important thing here is that you will $match against the first collection (users or whatever the collection name is in your environment) and then use $lookup to perform the "match" against the subsequent launches collection.
Here is a playground demonstrating the basic approach. Adjust as needed to the specifics of your environment.

Model.create() from Mongoose doesn´t save the Documents in my Collection

I have created a sigle app with a Schema and a Model to create a Collection and insert some Documents.
I have my todoModel.js file:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const todoSchema = new Schema({
username: String,
todo: String,
isDone: Boolean,
hasAttachment: Boolean
});
const Todos = mongoose.model("Todo", todoSchema);
module.exports = Todos;
Then I have created a setUpController.js file with a sample of my Documents. Then I create a Model and I pass my sample of Documents and my Schema. I create a response to send tje result in JSON.
Everything good here, as I get the result in json when accessing to the route.
Here is the code:
Todos.create(sampleTodos, (err, results) => {
if (!err) {
console.log("setupTodos sample CREATED!")
res.send(results);
}
else {
console.log(`Could not create the setupTodos Database sample, err: ${err}`);
}
});
My problem is that this Documents don´t get saved in the collection !! When I access to the database, nothing is there.
This is my app.js file:
mongoose.connect("mongodb://localhost:27017/nodeTodo")
.then(connection => {
app.listen(port);
})
.catch(err => {
console.log(`Could not establish Connection with err: ${err}`);
});
Could anyone help me please ?
Thank you
Try creating an instance and making the respective function call of that instance. In your case, save the document after creating an instance and it works like a charm.
const newTodos = new Todos({
username: "username",
todo: "todos",
isDone: false,
hasAttachment: flase
});
const createdTodo = newTodos.save((err, todo) => {
if(err) {
throw(err);
}
else {
//do your staff
}
})
after the collection is created you can use the function inserMany to insert also a single document the function receives an array of objects and automatically saves it to the given collection
example:
Pet = new mongoose.model("pet",schemas.petSchema)
Pet.insetMany([
{
//your document
}])
it will save only one hardcoded document
I hope it was helpful

Unit Testing with Jest for Strapi v4

I am trying to perform unit tests with Jest for the new version of Strapi, v4 which was just released a couple of weeks ago. In accordance with their documentation, the old guide for unit testing no longer runs as expected. I have, however, modified the code to work to a certain extent. Currently I have the following:
./test/helpers/strapi.js:
const Strapi = require("#strapi/strapi");
let instance;
async function setupStrapi() {
if (!instance) {
/** the following code in copied from `./node_modules/strapi/lib/Strapi.js` */
await Strapi().load();
instance = strapi; // strapi is global now
await instance.server
.use(instance.server.router.routes()) // populate KOA routes
.use(instance.server.router.allowedMethods()); // populate KOA methods
await instance.server.mount();
}
return instance;
}
module.exports = {
setupStrapi
};
./tests/app.test.js:
const fs = require("fs");
const { setupStrapi } = require("./helpers/strapi");
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
const dbSettings = strapi.config.get("database.connection.connection");
//close server to release the db-file
await strapi.server.destroy();
//DATABASE_FILENAME=.tmp/test.db
//delete test database after all tests
if (dbSettings && dbSettings.filename) {
const tmpDbFile = `${dbSettings.filename}`;
if (fs.existsSync(tmpDbFile)) {
fs.unlinkSync(tmpDbFile);
}
}
});
it("should return hello world", async () => {
await request(strapi.server.httpServer).get("/api/hello").expect(200); // Expect response http code 200
});
./config/env/test/database.js
const path = require("path");
module.exports = ({ env }) => ({
connection: {
client: "sqlite",
connection: {
filename: path.join(
__dirname,
"../../../",
env("DATABASE_FILENAME", ".tmp/test.db")
),
},
useNullAsDefault: true,
},
});
The route /api/hello is a custom API endpoint. This works perfectly when running strapi develop, and all permissions are set correctly.
The tests run, but every endpoint that is not / or /admin returns 403 Forbidden, meaning there is a problem with the permissions. It would seem that the database file .tmp/data.db (used in development) is not replicated correctly in .tmp/test.db. In other words, this is close to working, but the permissions for API endpoints are not set correctly.
I have been searching through StackOverflow and the Stapi Forums over the past few days but to no avail. I would greatly appreciate some pointers as to how to fix this :)
It seems you need to grant the right privileges to your routes on your test DB.
For that you can create a function, lets call it grantPriviledge, and call it in your test in the function beforeAll.
// Here I want to grant the route update in my organization collection
beforeAll(async () => {
await grantPrivilege(1, 'permissions.application.controllers.organization.update');
});
And here is the function grantPriviledge:
// roleID is 1 for authenticated and 2 for public
const grantPrivilege = async (roleID = 1, value, enabled = true, policy = '') => {
const updateObj = value
.split('.')
.reduceRight((obj, next) => ({ [next]: obj }), { enabled, policy });
const roleName = roleID === 1 ? 'Authenticated' : 'Public';
const roleIdInDB = await strapi
.query('role', 'users-permissions')
.findOne({ name: roleName });
return strapi.plugins['users-permissions'].services.userspermissions.updateRole(
roleIdInDB,
updateObj,
);
};
Let me know if that helps
So in order for that to work in v4, this is how I did.
This was based in this and this but Stf's posting in this saying "inject them in your database during the bootstrap phase like it is made in the templates" was what really set me in the right track.
So if you look here you will see this function:
async function setPublicPermissions(newPermissions) {
// Find the ID of the public role
const publicRole = await strapi
.query("plugin::users-permissions.role")
.findOne({
where: {
type: "public",
},
});
// Create the new permissions and link them to the public role
const allPermissionsToCreate = [];
Object.keys(newPermissions).map((controller) => {
const actions = newPermissions[controller];
const permissionsToCreate = actions.map((action) => {
return strapi.query("plugin::users-permissions.permission").create({
data: {
action: `api::${controller}.${controller}.${action}`,
role: publicRole.id,
},
});
});
allPermissionsToCreate.push(...permissionsToCreate);
});
await Promise.all(allPermissionsToCreate);
}
Later on the code, this function is called like this:
await setPublicPermissions({
article: ["find", "findOne"],
category: ["find", "findOne"],
author: ["find", "findOne"],
global: ["find", "findOne"],
about: ["find", "findOne"],
});
So in my case I modified this function a bit to accept between authenticated (1) and public (2) roles inspired by Sidney C answer above.
This is how I did it:
const grantPrivilege = async (roleID = 1, newPermissions) => {
const roleName = roleID === 1 ? "authenticated" : "public";
// Find the ID of the public role
const roleEntry = await strapi
.query("plugin::users-permissions.role")
.findOne({
where: {
type: roleName,
},
});
// Create the new permissions and link them to the public role
const allPermissionsToCreate = [];
Object.keys(newPermissions).map((controller) => {
const actions = newPermissions[controller];
const permissionsToCreate = actions.map((action) => {
return strapi.query("plugin::users-permissions.permission").create({
data: {
action: `api::${controller}.${controller}.${action}`,
role: roleEntry.id,
},
});
});
allPermissionsToCreate.push(...permissionsToCreate);
});
await Promise.all(allPermissionsToCreate);
};
And then in my beforeAll block I call it like this:
await grantPrivilege(1, {
"my-custom-collection": ["create", "update"],
category: ["find", "findOne"],
author: ["find", "findOne"],
});

Searching Multiple parts of a Mongodb model

This is my search route:
router.get("/search", async (req, res)=>{
let search = {}
if(req.query.q !=null && req.query.q!==""){
search.title = new RegExp(req.query.q, "i")
search.description = new RegExp(req.query.q, "i")
}
try{
console.log(search)
const posts = await Post.find(search)
res.render("posts/search", {posts: posts, search: req.query})
}catch(err){
console.log(err)
}
})
When I send my query like this, my console returns me this value:
{ description: /whateverIamsearchingfor/i, tags: /whateverIamsearchingfor/i }
And when I search only one of them and comment out the other line I get this: (no surprises)
{ description: /whateverIamsearchingfor/i}
Naturally, It works when only I search for either titles or descriptions. How do I fix this? I want to be able to make a search on both of these parts of this model at the same time. (or maybe even search the other parts as well)
Yay. I made it. In case anybody is still wondering how, I'll post it here:
const posts = await Post.find({ "$or": [
{ title: search.title },
{ description: search.description }
] });
Works like a charm now.

Mongoose updateOne() going through okay but not updating

I have this request:
// PUT that updates a user.
router.put('/api/user/:id', async (req: Request, res: Response) => {
const { email, name, avatar } = req.body
const userId = req.body._id
const conditions = {
_id : userId
}
const user = {$set: { "email": email, "name": name, "avatar": avatar } }
User.updateOne(conditions, user).then(doc => {
if (!doc) { return res.status(404).end() }
return res.status(200).json(doc)
}).catch(error => console.log(error))
})
And I get this response from the request:
{
"n": 0,
"nModified": 0,
"ok": 1
}
If you can find it on StackOverflow about the updateOne() method in mongoose I've probably tried it. The document isn't updating no matter what I try.
Edit: I've tried using an ObjectID in the query instead and the same result.
Edit 2: I figured it out. Was using req.body.id instead of req.params.id and I was using parameters to send the request. Thanks everyone for the help!
nModified == 0 implies that you have no user matching this id,
your route is put /api/user/:id but your user id is in req.params.id and not in req.body._id
A couple tips:
Try running the same query from mongodb at the command line, see if you get any results.
Is the "campaign_id" defined as an ObjectId in your schema? If so, try searching using the ObjectId type.
Try to change the query to :
const ObjectId = require('mongoose').Types.ObjectId;
const conditions = {
_id : new ObjectId(userId)
}
The reason for not updating is - mongoose is unable to search the with the id you provided.
if you want to update a document based on _id you can use findByIdAndUpdate()
const userId = req.body._id;
const user = { "email": email, "name": name, "avatar": avatar }
User.findByIdAndUpdate(userId , user,
function (err, docs) {
if (err){
console.log(err)
}
else{
console.log("Updated User : ", docs);
}
});
In case you've set your DB to strict mode don't forget to add strict:false in options when adding new keys. Otherwise, inserts will be silently ignored. I've just spent 2 hours wondering why my inserts don't get saved in DB despite not throwing any error.
See dos
http://mongoosejs.com/docs/guide.html#strict
const conditions = {
_id
}
const dateToUpdate = {
$set: {
"email": "email",
"name": "name",
"avatar": "avatar"
}
}
const updateRecord = await models.pdDealModel.updateOne(conditions,dateToUpdate,{
upsert:false,
strict:false
}
)

Categories

Resources