NodeJS - TypeScript - URL Query doesn't work if multiple parameters - javascript

I have the following controller in my route:
export const getHotels = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const hotels = await Hotel.find(req.query).limit(+req.query.limit);
res.status(200).json(hotels);
} catch (err) {
next(err);
}
};
In my database, I have hotels with a featured property (boolean) that I retrieve with Mongoose and reduce the results with its limit method.
I noticed that my query returns an empty array if I have several parameters no matter what if I call (GET): /api/hotels?featured=true&limit=1
It works fine if the controller is await Hotel.find().limit(+req.query.limit); and the URL /api/hotels?limit=1
Or if controller is await Hotel.find(req.query); and URL /api/hotels?featured=true
Does anyone have an idea of what could be the issue? Many thanks.

When your GET request is /api/hotels?featured=true&limit=1, the req.query content is:
{
featured: "true",
limit: "1"
}
...therefore the Hotel.find(req.query) looks for documents with both "featured" field as "true" AND "limit" field as "1"... hence it does not find anything.
You could make sure to extract only necessary fields, e.g. with Lodash pick utility:
Hotel
.find(pick(req.query, ["featured"]))
.limit(+req.query.limit)

I realised I wasn't using the object bracket with find() before which is why mongoDB could only take the 1st argument.
Example:
const { limit, ...others } = req.query;
const hotels = await Hotel.find({
...others,
}).limit(+limit);
By doing so, I can add any parameter in my GET request and don't need to use Lodash to pick a specific parameter.

Related

How to set a key in Redis and get the value (I'm building a url shortener)

I'm kind of new to Redis and I'm currently experiencing a project stand-still because I don't know any other way to set and get in Redis.
My problem is I'm building a url shortener and when the user posts (a POST request) a url to the server, I'm setting the url as the key and a nanoid generated code as the value and sending back the nanoid code to the user. But when the user sends a GET request with the url code to the server I have to check if the url is already cached and redirect the user to the url but I can't because the actual url as been set as the key not the url code so it will always return undefined. Please can you help me with this problem? Is there some other to do this? Many thanks in advance! Here is the code:
import redis from 'redis';
import http from 'http';
import express from 'express';
import { Router } from 'express';
import { promisify } from 'util';
import { nanoid } from 'nanoid';
interface Handler {
(req: Request, res: Response, next: NextFunction): Promise<void> | void;
}
interface Route {
path: string;
method: string;
handler: Handler | Handler[];
}
const { PORT = 8080} = process.env;
// I'm using a docker container
const { REDIS_URL = 'redis://cache:6379' } = process.env;
const redisClient = redis.createClient({
url: REDIS_URL
});
const initCache = async () =>
new Promise((resolve, reject) => {
redisClient.on('connect', () => {
console.log('Redis client connected');
resolve(redisClient);
});
redisClient.on('error', error => reject(error));
});
async function getShortenedURL(url: string) {
const urlCode = nanoid(7);
redisClient.setex(url, 3600, urlCode);
return urlCode;
}
const getAsync = promisify(redisClient.get).bind(redisClient);
async function getFromCache(key: string) {
const data = await getAsync(key);
return data;
}
const routes = [
{
path: '/:url',
method: 'get',
handler: [
async ({ params }: Request, res: Response, next: NextFunction) => {
try {
const { url } = params;
const result = await getFromCache(url);
if (result) {
res.redirect(301, result);
} else {
throw new Error('Invalid url');
}
} catch (error) {
console.error(error);
}
}
]
},
{
path: '/api/url',
method: 'post',
handler: [
async ({ body }: Request, res: Response, next: NextFunction) => {
const { url } = body;
const result = await getFromCache(url);
result ? res.status(200).send(`http://localhost:${PORT}/${result}`) : next();
},
async ({ body }: Request, res: Response) => {
const result = await getShortenedURL(body.url as string);
res.status(200).send(result);
}
]
}
];
const applyRoutes = (routes: Route[], router: Router) => {
for (const route of routes) {
const { method, path, handler } = route;
(router as any)[method](path, handler);
}
};
const router = express();
applyRoutes(routes, router);
const server = http.createServer(router);
async function start() {
await initCache();
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}...`)
}
);
}
start();
As I understand, you need to make sure that you do not shorten and store any given url twice.
You could encode the url and use it as the sort version and as a key at the same time. E.g.
www.someurltoshorten.com -> encoded value ->
{key: value} -> encoded value: www.someurltoshorten.com
If a user wants to shorten a url, you encode it first and you should get the exact same hash for the exact same url.
Once you get the encoded value, you can use the SET command with a "GET" option. You can also use the expire (EXAT) option to clean up old urls (those that nobody is looking for anymore) using the feature that is built in Redis.
It will do the following for you:
Set key to hold the string value (the key is the short version of the url and the value is the url itself)
If the value exists, it will overwrite it and reset (extend) the TTL (time to live) if you set it.
And the "GET" option will return the old value if it exists or null.
With one command you will be able to:
Create a value in Redis
Get the value if it already exists resetting the TTL (it makes sense to extend it) and all of the without any extra code with one command only!!!
The flow may look as follows:
A user inputs a url to be shortened:
you encode the url
you store it in Redis using the SET command where the key is the encoded value and the value is the url.
you return the encoded value which you already now. There is no need to check whether the url has already been shortened once because the SET command will either create a new entry or update the existing once.
A user inputs a shortened url
you encode the url
you store it in Redis using the SET command where the key is the encoded value and the value is the url.
you get the url from the value that was returned by the SET command thank to the "GET" option.
The only difference between the two cases is in whether you return the shortened url or the normal url
Basically, you need one Redis command for all of that to work.
I did not test the encoding/hashing of the url and it may not work with all types of url. You need to check which encoding would cover all cases.
But the idea here is the concept itself. It's similar to how we handle passwords. When you register, it's hashed. Then, when you log in and provide the same password, we can hash it again and compare hashes. Secure hashing with bycript, as an example, can be expensive (can take a lot of time).
For urls you need to make sure that encoding/hashing always produces the same result for the same url.
Keep in mind the length of the keys as describe here https://redis.io/topics/data-types-intro#redis-keys
you should use the HashCode generate for the URL as the Key for your dictionary since you intend to lookup by the shortened URL later.
Post--> Hash the URL, Encode it as per your need for length restrictions return the shortened Key as shortened URL and put <Hash,URL> in your map
Get--> User gives the shortened Key, Dictionary lookup for shortened Key and return the actual URL.

Next.js & MongoDB (without Mongoose) returning empty array

I'm using API routes in Next.js with sample data from MongoDB. I am trying to return the data from a single object. First time working with MongoDB, so apologies if the answer has been staring me in the face.
I'm getting an empty array returned with the following query:
import { connectToDatabase } from '../../../util/mongodb';
export default async (req, res) => {
const { db } = await connectToDatabase();
const movies = await db
.collection("movies")
.find({ "_id": "573a1395f29313caabce1f51" })
.limit(20)
.toArray();
res.json(movies);
};
Which corresponds to an object like this:
{
"_id": "573a1395f29313caabce1f51",
"fullplot": "some text goes here"
}
What am I missing? Shouldn't .find({_id: "573a1395f29313caabce1f51"}) return that information? Why am I only seeing an empty array? I understand why it's returning an array (I added .toArray()), but that shouldn't impact whether the results are returned or not.
For what it's worth, querying without parameters works properly. No issues with this query:
import { connectToDatabase } from '../../util/mongodb';
export default async (req, res) => {
const { db } = await connectToDatabase();
const movies = await db
.collection("movies")
.find({})
.sort({ metacritic: -1 })
.limit(20)
.toArray();
res.json(movies);
};
Any help is appreciated, thanks in advance!
Needed to cast to an objectID as mentioned in the comments.
https://docs.mongodb.com/manual/reference/operator/aggregation/toObjectId/

check if object in array of objects - javascript

I know i have to use some but for some reason i cant seem to get it right. i have a collection in my mongodb database of posts. each post has an array of objects named "likes" that references the users that liked this post. so in my backend i want to check if the user exists in the likes array of the post. if it does not exist then like the post, else return with an appropriate message on my react frontend. The code i will include always returns false from some so a user can like a post infinite times.
exports.postLike = async (req, res, next) => {
const postId = req.query.postId;
const userId = req.query.userId;
console.log('postId: ' + postId);
try{
const post = await Post.findById(postId).populate('creator').populate('likes');
const user = await User.findById(userId);
if (!post.likes.some(post => post._id === user._id)){
post.likes.push(user);
console.log('liked a post');
const result = await post.save();
res.status(200).json({ message: 'Post liked!', post: result });
} else {
console.log('Post already liked!');
res.status(200).json({ message: 'Post already liked!', post: post });
}
}catch (err) {
if (!err.statusCode) {
err.statusCode = 500;
}
next(err);
}
};
i clearly haven't understood, yet, how some works so if you can help that would be great. also if you have any other solution that would be good in this case then please post it. i tried some random codes with indexOf and includes for checking but it didn't work either. i am not sure which is the right way to check if the user object is included in the "likes" array of objects. i would prefer not to write any function of my own to check this, i want to do it using an existing function/method provided by javascript.
Going to offer a different route here. You are fetching all the data including a join to the creator and likes just to add a like to the collection. This is a little wasteful and can be achieved by just doing an update and use $addToSet which will add the like if it does not exist.
You then just check nModified in the result to know if it was added or not. So you can have:
const result = await Post.updateOne(
{
id: 1
},
{
$addToSet: {
likes: {
userId: mongoose.Types.ObjectId(req.query.userId)
}
}
}
);
console.info(result.nModified === 1);
Alternatively, you can use some as follows using === to compare type and value:
posts.likes.some(like => like.userId.toString() === req.query.userId)
MongoDB.ObjectId is a wrapper around a primitve, just like Number or Boolean. And just like
new Boolean(true) === new Boolean(true)
will be false, your comparison will fail too. You have to take out the primitive for comparison:
post._id.valueOf() === user._id.valueOf()

How can I insert an object graph by using MikroORM?

I'm trying to create and update multiple entities (models) at once. I did this in objection ORM by using insertGraph API which actually inserts entity if it has no id and updates if it has id.
Is there a similar API in MikroORM?
Currently I'm doing this:
app.put('/articles', async (req, res) => {
const save = req.body.articles.map(async (dto) => {
const article = Object.assign(new Article(), dto)
await req.em.persistAndFlush(article)
})
await Promise.all(save)
res.send({ ok: true })
})
but it generates multiple transactions and I want to everything in single transaction.
The problem here is that when using persistAndFlush method, you immediately persist the entity to database by awaiting the promise. Instead you can call em.persistLater(article) to mark it for persisting. Then call em.flush() afterwards, which will commit all changes to database inside single transaction.
app.put('/articles', async (req, res) => {
req.body.articles.forEach(dto => {
const article = Object.assign(new Article(), dto)
req.em.persistLater(article)
})
await req.em.flush() // save everything to database inside single transaction
res.send({ ok: true })
})
You can make it even simpler by preparing all entities into one array, and persistAndFlush that instead:
app.put('/articles', async (req, res) => {
const articles = req.body.articles.map(dto => Object.assign(new Article(), dto))
await req.em.persistAndFlush(articles) // save everything to database inside single transaction
res.send({ ok: true })
})
Also, instead of using Object.assign(), you can use IEntity.assign() method on the entity, which will also take care of creating references from plain identifiers:
const article = new Article().assign(dto)
More about IEntity.assign() can be found in the docs:
https://b4nan.github.io/mikro-orm/entity-helper/
You could also use EntityManager.create() helper, which will construct the entity for you - the benefit of this is that it will automatically handle constructor parameters, passing them to constructor instead of assigning them directly.
const article = req.em.create(Article, dto)

Sequelize join on where condition returned from first table or return object values in an array derrived from foreach coming up empty

I've been trying to figure out this for a while now so any help would be very much appreciated.
I have one table called Interaction that searches with the client user's id and returns all interactions where they are the target user. Then I want to return the names of those users who initiated the interaction through the User table.
I tried using include to join the User table but I can't get the user's names using the where clause because it is based on a value returned in the first search of the Interaction table and don't know if I can search on a value that isn't the primary key or how?
The closest I've gotten is to use foreach and add the users to an array but I can't get the array to return in my response, because outside of the loop it is empty. I've tried suggestions I've found but can't figure out how to return the array outside of the foreach, if this is the best option. I am sure it is something really stupid on my behalf. TIA.
This is my attempt at include function:
getInvited: (req, res, next) => {
var user = {}
user = req.user;
let usrId = user[0]['facebookUserId'];
var userObjArray = [];
Interaction.findAll({
where: {
targetUserId: usrId,
status: 'invited',
},
include: [{
model: User,
attributes: [
'firstName'
],
where: {
facebookUserId: IwantToJoinOnInteraction.userId // replace with working code?
}]
}).then(function (users) {
res.send(users);
}).catch(next);
}
Or my attempt at foreach:
getInvited: (req, res, next) => {
var user = {}
user = req.user;
let usrId = user[0]['facebookUserId'];
var userObjArray = [];
Interaction.findAll({
where: {
targetUserId: usrId,
status: 'invited',
}
}).then(function (interactions) {
interactions.forEach((interaction) => {
User.findOne({
where: {
facebookUserId: interaction.userId // this is the where clause I don't know how to add in my first attempt with include
},
attributes: ['firstName', 'facebookUserId']
}).then(function (user) {
userObjArray.push(user['dataValues']);
console.log(userObjArray); // on the last loop it contains everything I need
})
})
res.status(200).send(userObjArray); // empty
}).catch(next);
},
You have to wait for all promises before sending the response. Your code runs async. With the forEach you are calling User.findOne async but you don't wait for all User.findOne to finish. A convenient way to make this work is Promise.all. You can pass an array of promises and the returned promise resolves to an array of all the resolved promises.
Promise.all(interactions.map(interaction => User.findOne(...)))
.then(users => {
res.status(200).send(users.map(user => user.dataValues))
})
You could write this much more easy to read woth async/await
getInvited: async (req, res, next) => {
...
const interactions = await Interaction.findAll(...)
const users = await Promise.all(interactions.map(interaction => User.findOne(...)))
res.status(200).send(users.map(user => user.dataValues))
}

Categories

Resources