Find users that follow each other on a Prisma self-relation - postgresql

Given a user id, I want to find the followers that the user follows (i.e. follow each other)
My Prisma model looks like the following:
model User {
id Int #id #default(autoincrement())
name String?
followedBy Follows[] #relation("following")
following Follows[] #relation("follower")
}
model Follows {
follower User #relation("follower", fields: [followerId], references: [id])
followerId Int
following User #relation("following", fields: [followingId], references: [id])
followingId Int
##id([followerId, followingId])
}
I am also interested in counting them - this could be done in a separate query since the former might require pagination at some point.
Thank you in advance for your help.

Probably a raw query will do
function getFollowersMutual(userId: User["id"]) {
return prisma.$queryRawUnsafe<User[]>(
`SELECT u.* FROM "Follows" AS f1
INNER JOIN "Follows" AS f2
ON f2."followingId" = f1."followerId"
INNER JOIN "User" AS u
ON f1."followerId" = u."id"
WHERE f1."followingId" = '${userId}`
);
}
and counting:
function getFollowersMutualCount(userId: User["id"]) {
return Number(
(await prisma.$queryRawUnsafe<{ count: number }[]>(
`SELECT COUNT(*) FROM "Follows" AS f1
INNER JOIN "Follow" AS f2
ON f1."followingId" = f2."followerId"
WHERE f1."followingId" = '${userId}'
AND f2."followingId" = f1."followerId"`
))[0].count
)
}
sadly it seems that Prisma's queryRaw doesn't support AS so I had to use queryRawUnsafe and need to sanitize the input (not done in the code above).

To find the mutual followers you can do something like this:
import { PrismaClient } from '#prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.createMany({
data: [
{
id: 1,
name: 'User 1',
},
{
id: 2,
name: 'User 2',
},
{
id: 3,
name: 'User 3',
},
],
});
// // User 1 follows User 2 and User 3
// // User 2 follows User 1
await prisma.follows.createMany({
data: [
{
followerId: 1,
followingId: 2,
},
{
followerId: 1,
followingId: 3,
},
{
followerId: 2,
followingId: 1,
},
],
});
// For User id 1, get all the followers and all the users they follow
const users = await prisma.follows.findMany({
where: {
OR: [
{
followerId: 1,
},
{
followingId: 1,
},
],
},
include: {
following: true,
},
});
console.log(users);
const mutuals = [];
// For each user, check if they follow each other
for (let i = 0; i < users.length; i++) {
const user = users[i];
for (let j = i + 1; j < users.length; j++) {
const innerUser = users[j];
if (
user.followerId === innerUser.followingId &&
user.followingId === innerUser.followerId
) {
mutuals.push(user);
}
}
}
console.log(mutuals);
}
main()
.catch((e) => {
throw e;
})
.finally(async () => {
await prisma.$disconnect();
});
This requires some post processing of records to find the mutual follows of each other.
Here's the output:
[
{
followerId: 1,
followingId: 2,
following: { id: 2, name: 'User 2' }
}
]

Related

Mongodb: How to load 'pages' of field of type array from the end of the array until its beginning

I am displaying messages in infinite scroll. In other words, I load set by set and display them using this function:
const getXNumberOfMessages = async (
user_id,
conv_id,
page,
results_per_page
) => {
results_per_page = parseInt(results_per_page);
const conversation = await Conversation.findOne({
_id: conv_id,
members: { $in: [user_id] },
}).populate({
path: "messages",
options: {
skip: results_per_page * page,
limit: results_per_page,
},
});
let messages = conversation.messages.map((message) => {
message.text = encryptionServices.decryptPrivateMessage(message.text);
return message;
});
return messages;
};
The problem is that messages as you know get loaded from the last set until the first set.
Whereas that function does the opposite.
It load the the messages from the first set until the last set.
Any idea how to achieve my goal?
So I managed to solve this like this:
const getXNumberOfMessages = async (
user_id,
conv_id,
page,
results_per_page
) => {
results_per_page = parseInt(results_per_page);
const conversation = await Conversation.findOne({
_id: conv_id,
members: { $in: [user_id] },
}).populate({
path: "messages",
options: {
skip: results_per_page * page,
limit: results_per_page,
sort: { date: -1 },
},
});
let messages = conversation.messages.map((message) => {
message.text = encryptionServices.decryptPrivateMessage(message.text);
return message;
});
messages.reverse();
return messages;
};

Merge or 'reference' and object into another through GraphQL?

Very new to graphQL (and MongoDB), and I am wondering how to reference an another object in graph QL.
I have two objects in two collections in MongoDB...
{
_id: 1,
companies: [
{
id: 1,
name: 'Google'
},
{
id: 2,
name: 'Apple'
}
]
}
{
_id: 2,
jobs: [
{
id: 1,
title: 'Designer'
},
{
id: 2,
name: 'Developer'
}
]
}
The Designer job is posted by google, and I want to include that in the returned object from GraphQL (I am using the 'id: 1' as a reference I guess? Presume ObjectID might be the way to go instead tho )
How would I go about that?
Ideally I want to return
{
"data": {
"job": {
"id": 1,
"title": "Designer",
"company": {
id: 1,
name: "Google"
}
}
}
}
But not sure how to go about it, I currently have...
resolvers.js
export const resolvers = {
Query: {
jobs: async (_parent, {}, context) => {
const jobs = await context.db
.collection('jobs')
.findOne()
return jobs.jobs
},
companies: async (_parent, {}, context) => {
const companies = await context.db
.collection('companies')
.findOne()
return companies.companies
},
job: async (_parent, { id }, context) => {
const job = await context.db.collection('jobs').findOne()
return job.jobs.find((job) => job.id === Number(id))
},
},
}
typeDefs.js
export const typeDefs = gql`
type Job {
_id: ID
id: Int
title: String
}
type Company {
_id: ID
id: Int
name: String
}
type Query {
jobs: [Job]
companies: [Company]
job(id: Int): Job
}
`
But not sure how to tie these in together? I am using Apollo / GraphQL / MongoDB / NextJS and essentially set up very close to this
Thanks in advance for any help or guidance!

How to pass element index in array to mongoDB query?

Building cart on website and when product is added i want to first check if it is already in cart, if yes increment quantity by 1, if not add it. Cart is an array of objects and i want to pass index of object that contains added product to increment function but can't figure out how to do so.
async function add(product, userId) {
const user = await User.findById(userId);
const product = isProductInCart(product, user.cart); // returns true and index in cart if found
if (product.found === true) {
await User.findOneAndUpdate(
{ _id: userId },
{ $inc: { cart[product.index].quantity : 1 }} // not working
);
} else {
await User.findOneAndUpdate({ _id: userId }, { $push: { cart: product } });
}
}
function isProductInCart(product, cart) {
let productFound = { found: false, index: -1 };
for (let i = 0; i < cart.length; i++)
if (cart[i].name === product.name) {
productFound.found = true;
productFound.index = i;
break;
}
return productFound;
}
It looks like your code can be simplified if you consider using the $ positional operator:
let userWithCart = User.findOneAndUpdate(
{ _id: user, 'cart.name': product.name },
{ $inc: { 'cart.$.quantity' : 1 }}
)
if(!userWithCart ){
await User.findOneAndUpdate({ _id: userId }, { $push: { cart: product } });
}
First findOneAndUpdate will return no value when there's no corresponding cart.name (and you need to $push it). Otherwise MongoDB will automatically match the cart you want to update based on cart.name condition and increment the quantity subfield.
EDIT:
If you still need to proceed the way you've started you just need to evaluate the path in JavaScript:
{ $inc: { [`cart.${product.index}.quantity`] : 1 }}

Mongoose: is it possible to combine many database calls into one?

I've got 4 database operations in 1 API call, is it excessive? Can/should they be combined somehow?
// 1) Get the id for the user I want to follow
const account = await userModel
.findOne({ 'shared.username': username })
.exec();
if (!account) throw new Error('User not found');
// 2) Save the user id I want to follow and my Id into the following collection
const followed = new followingModel({
followingId: account.id,
userId: id,
} as FollowingModelInterface);
const saved = await followed.save();
// 3) Increment the number of people I'm following
const addFollowing = await userModel
.findOneAndUpdate(
{ _id: id },
{
$inc: { 'shared.following': 1 },
},
{
new: true,
}
)
.exec();
// 4) Increment the number of people who are following the user i just followed
const addFoller = await userModel
.findOneAndUpdate(
{ _id: account.id },
{
$inc: { 'shared.followers': 1 },
},
{
new: true,
}
)
.exec();
It feels like allot of trips to the database but maybe it's ok I'm not sure?
Maybe 3 and 4 dont need await and I can send a response to the front end after 2??
Maybe I'm overthinking it??
First and fourth operations look like that can be combined like this:
const account = await userModel.findOneAndUpdate(
{ "shared.username": username },
{
$inc: { "shared.followers": 1 }
},
{ new: true }
);
Or you can combine 3 userModel related update by using mongoose Model.bulkWrite

Null response in query for Relay Modern interogation on GraphQL

Maybe someone who has managed to pass this step is willing to provide some indications.
I have a schema, a resolver, i request the query and i have a null response.
Please can you help on topic?
module.exports = {
Query: {
allLinks: async (root, {filter}, {mongo: {Links, Users}}) => {
let query = filter ? {$or: buildFilters(filter)} : {};
return await Links.find(query).toArray();
}
and the query request looks like this:
query LinkListPageQuery {
allLinks {
...LinkList_allLinks
}
}
fragment LinkList_allLinks on LinkConnection {
edges {
cursor
...Link_link
}
}
fragment Link_link on LinkEdge {
node {
id
description
url
}
}
My schema looks like this:
const typeDefs = `
type Link implements Node {
id: ID!
url: String!
description: String!
postedBy: User
votes: [Vote!]!
}
interface Node {
id: ID!
}
type Query {
allLinks(filter: LinkFilter, first: Int): [LinkConnection]
node(
id: ID!
): Node
}
type LinkEdge {
node: Link!
cursor: String
}
type LinkConnection {
pageInfo: PageInfo
edges: LinkEdge
count: Int
}
input LinkFilter {
OR: [LinkFilter!]
description_contains: String
url_contains: String
}
}
`;
PS: This language schema is done according to graphql-tools package.
Resolver:
Query: {
users: async (root, { first, after }, { mongo: { Users }, user }) => {
const queryData = await Users.find(query).toArray();
first = first || queryData.length;
after = after ? parseInt(fromCursor(after), 10) : 0;
const edges = queryData.map((node, i) => ({
cursor: toCursor(i+1),
node: node._id,
})).slice(after, first + after);
const slicedUser = edges.map(({ node }) => node);
return {
edges,
pageInfo: {
startCursor: edges.length > 0 ? edges[0].cursor : null,
hasNextPage: first + after < queryData.length,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
},
count: queryData.length,
};
},
UserConnection: {
edges: ({ edges }) => edges,
pageInfo: ({ pageInfo }) => pageInfo,
count: ({ count }) => count,
},
UserEdge: {
node: async ({ node },data, {dataloaders: {userLoader}}) => {
const user = await userLoader.load(node);
return user;
},
cursor: ({ cursor }) => cursor,
},