Many-to-many Self Relation Prisma - One Field - many-to-many

I'm trying to create a friendship mechanic for my app using Prisma among other tools. In the docs it shows the following example for how to create a many-to-many self relation:
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 have implemented this and it works, however the issue is that for friendships, there is no 'following' and 'followedBy', you're just friends. At the moment, when I query, I have to query both fields in order to find all of a user's friends. Is there any way to define this type of relationship with only one field? Whereby we just have a single list of friends on a user?

I agree that it would be nice if Prisma could more natively support this sort of self-relation where the relationship is expected to be symmetric (e.g. userA is friends with userB if and only if userB is friends with userA).
However, as far as I can tell Prisma insists on having two "sides" of the relationship. (If someone knows better, I would love to hear it!) So what follows is the approach I am taking, which avoids having to query both relations to find a user's full set of friends.
Concept
We'll use one "side" of the relation to contain the complete set of friends. The other "side" exists solely to meet Prisma's requirement, and we'll never query it directly.
When adding or removing a friend relationship, we'll make two prisma calls, one to update each object.
Code
Schema file:
model User {
id Int #id #default(autoincrement())
name String?
friends User[] #relation("UserFriends")
// This second "side" of the UserFriends relation exists solely
// to satisfy prisma's requirements; we won't access it directly.
symmetricFriends User[] #relation("UserFriends")
}
Methods to add and remove friendships (there's plenty of redundant code in here that could be abstracted out, but I think it's clearer to read this way):
const addFriendship = async (userIdA: string, userIdB: string) => {
await prisma.user.update({
where: {id: userIdA},
data: {friends: {connect: [{id: userIdB}]}},
});
await prisma.user.update({
where: {id: userIdB},
data: {friends: {connect: [{id: userIdA}]}},
});
};
const removeFriendship = async (userIdA: string, userIdB: string) => {
await prisma.user.update({
where: {id: userIdA},
data: {friends: {disconnect: [{id: userIdB}]}},
});
await prisma.user.update({
where: {id: userIdB},
data: {friends: {disconnect: [{id: userIdA}]}},
});
}
With this approach, one can load a user and get all their friends in the expected manner, e.g.
const getUserWithFriends = async (userId) =>
await prisma.user.find({
where: {id: userId},
include: {friends: true},
});

Related

Prisma many to many self relation error due to #relation and generated type input

Using Prisma 3.7.0.
I'm following this example in the docs
model Person {
id Int #id #default(autoincrement())
name String?
followers Follows[] #relation("follower")
following Follows[] #relation("following")
}
model Follows {
follower Person #relation("follower", fields: [followerId], references: [id])
followerId Int
following Person #relation("following", fields: [followingId], references: [id])
followingId Int
##id([followerId, followingId])
}
Then I try creating a User along with the people they're following, with the below code.
const person = await prisma.person.create({
data: {
following: {
create: [
{ following: { connect: { id: 1 } } }
]
}
}
});
I'm getting the following (see what I did there :)) error.
/usr/src/app/node_modules/#prisma/client/runtime/index.js:34755
const error2 = new PrismaClientValidationError(renderErrorStr(validationCallsite));
^
PrismaClientValidationError: Unknown arg `following` in data.following.create.0.following for type FollowsCreateWithoutFollowingInput. Did you mean `follower`?
Argument follower for data.following.create.0.follower is missing.
at Object.validate (/usr/src/app/node_modules/#prisma/client/runtime/index.js:34755:20)
at PrismaClient._executeRequest (/usr/src/app/node_modules/#prisma/client/runtime/index.js:39749:17)
at consumer (/usr/src/app/node_modules/#prisma/client/runtime/index.js:39690:23)
at /usr/src/app/node_modules/#prisma/client/runtime/index.js:39694:49
at AsyncResource.runInAsyncScope (node:async_hooks:199:9)
at PrismaClient._request (/usr/src/app/node_modules/#prisma/client/runtime/index.js:39694:27)
at request (/usr/src/app/node_modules/#prisma/client/runtime/index.js:39799:77)
at _callback (/usr/src/app/node_modules/#prisma/client/runtime/index.js:40007:14)
at PrismaPromise.then (/usr/src/app/node_modules/#prisma/client/runtime/index.js:40014:23) {
clientVersion: '3.7.0'
}
I believe the reason I'm getting this error is because the following[] relation creates an input as mentioned in the error message FollowsCreateWithoutFollowingInput, meaning it's expecting a follower relation and not a following, as the following will be the Person I'm currently creating, and I just need to tell it who is the follower Person.
However this doesn't make sense to me. When I'm creating a Person along with its people they're following, I understand that to be an array of Persons who the person I'm currently creating is following. If so, then a Follows record in the following array contains the current Person (the one I'm creating) as the follower relation and some other Person as the following relation. And the relation I should be inputting is the following and not the follower. Therefore the input type that prisma should generate should be FollowsCreateWithoutFollowerInput instead of FollowsCreateWithoutFollowingInput.
What am I missing, in my understanding?
I looked at the below resources during my research on this.
count self relation on Prisma error: table name specified more than once. This is discussing a different issue, using the same example.
https://github.com/prisma/prisma/discussions/3960. This discusses how to create the same type of relation, where the join table references the same table for both ids. But it doesn't explain how to create records once the relationships are defined.
One-to-many self-relation in prisma schema. This shows how to create a record, but its not using the join tables relation during create it's using a property. Also it's not exactly the same case as the join table seems to be the same table.
`const person = await prisma.person.create({
data: {
following: {
connect: { id: 1 }
}
}
});
It's the "create" in the body of the function that's doing it. The second nested following would also cause a problem if you just fix that though.
Think of it like you aren't creating the follower. The follower already needs to be there. You are just creating a record of the connection between the person and the follower. So prisma.person.create some data that this person has a follower and that follower is connected to the user with id whatever.

Create or update one to many relationship in Prisma

I'm trying to update a one to many relationship in Prisma. My schema looks like this
model A_User {
id Int #id
username String
age Int
bio String #db.VarChar(1000)
createdOn DateTime #default(now())
features A_Features[]
}
model A_Features {
id Int #id #default(autoincrement())
description String
A_User A_User? #relation(fields: [a_UserId], references: [id])
a_UserId Int?
}
I'm trying to add a couple of new features to user with id: 1, or update them if they are already there.
I'm trying doing something like
const post = await prisma.a_User.update({
where: { id: 1},
data: {
features: {
upsert: [
{ description: 'first feature'},
{ description: 'second feature'}
]
}
}
})
The compiler isn't happy, it tells me
Type '{ features: { upsert: { description: string; }[]; }; }' is not assignable to type '(Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput)'.
Object literal may only specify known properties, and 'features' does not exist in type '(Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput)'.ts(2322)
index.d.ts(1572, 5): The expected type comes from property 'data' which is declared here on type '{ select?: A_UserSelect; include?: A_UserInclude; data: (Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput); where: A_UserWhereUniqueInput; }'
(property) features: {
upsert: {
description: string;
}[];
}
I can't work out how to do it nor I can find clear help in the documentation. Any idea on how to implement it or where I can find some examples?
I'm providing my solution based on the clarifications you provided in the comments. First I would make the following changes to your Schema.
Changing the schema
model A_User {
id Int #id
username String
age Int
bio String #db.VarChar(1000)
createdOn DateTime #default(now())
features A_Features[]
}
model A_Features {
id Int #id #default(autoincrement())
description String #unique
users A_User[]
}
Notably, the relationship between A_User and A_Features is now many-to-many. So a single A_Features record can be connected to many A_User records (as well as the opposite).
Additionally, A_Features.description is now unique, so it's possible to uniquely search for a certain feature using just it's description.
You can read the Prisma Guide on Relations to learn more about many-to-many relations.
Writing the update query
Again, based on the clarification you provided in the comments, the update operation will do the following:
Overwrite existing features in a A_User record. So any previous features will be disconnected and replaced with the newly provided ones. Note that the previous features will not be deleted from A_Features table, but they will simply be disconnected from the A_User.features relation.
Create the newly provided features that do not yet exist in the A_Features table, and Connect the provided features that already exist in the A_Features table.
You can perform this operation using two separate update queries. The first update will Disconnect all previously connected features for the provided A_User. The second query will Connect or Create the newly provided features in the A_Features table. Finally, you can use the transactions API to ensure that both operations happen in order and together. The transactions API will ensure that if there is an error in any one of the two updates, then both will fail and be rolled back by the database.
//inside async function
const disconnectPreviouslyConnectedFeatures = prisma.a_User.update({
where: {id: 1},
data: {
features: {
set: [] // disconnecting all previous features
}
}
})
const connectOrCreateNewFeatures = prisma.a_User.update({
where: {id: 1},
data: {
features: {
// connect or create the new features
connectOrCreate: [
{
where: {
description: "'first feature'"
}, create: {
description: "'first feature'"
}
},
{
where: {
description: "second feature"
}, create: {
description: "second feature"
}
}
]
}
}
})
// transaction to ensure either BOTH operations happen or NONE of them happen.
await prisma.$transaction([disconnectPreviouslyConnectedFeatures, connectOrCreateNewFeatures ])
If you want a better idea of how connect, disconnect and connectOrCreate works, read the Nested Writes section of the Prisma Relation queries article in the docs.
The TypeScript definitions of prisma.a_User.update can tell you exactly what options it takes. That will tell you why the 'features' does not exist in type error is occurring. I imagine the object you're passing to data takes a different set of options than you are specifying; if you can inspect the TypeScript types, Prisma will tell you exactly what options are available.
If you're trying to add new features, and update specific ones, you would need to specify how Prisma can find an old feature (if it exists) to update that one. Upsert won't work in the way that you're currently using it; you need to provide some kind of identifier to the upsert call in order to figure out if the feature you're adding already exists.
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference/#upsert
You need at least create (what data to pass if the feature does NOT exist), update (what data to pass if the feature DOES exist), and where (how Prisma can find the feature that you want to update or create.)
You also need to call upsert multiple times; one for each feature you're looking to update or create. You can batch the calls together with Promise.all in that case.
const upsertFeature1Promise = prisma.a_User.update({
data: {
// upsert call goes here, with "create", "update", and "where"
}
});
const upsertFeature2Promise = prisma.a_User.update({
data: {
// upsert call goes here, with "create", "update", and "where"
}
});
const [results1, results2] = await Promise.all([
upsertFeaturePromise1,
upsertFeaturePromise2
]);

How to use .findAll and find records from two different tables and a self reference?

I'm working with two tables in particular. Users and Friends. Users has a bunch of information that defines the User whereas Friends has two columns aside from id: user_id and friend_id where both of them are a reference to the User table.
I'm trying to find all of the users friends in as little calls to the db as possible and I currently have 2. One to retrieve the id of a user first from a request, then another to Friends where I compare the IDs from the first call and then a third call that passes the array of friends and find all of them in the Users table. This already feels like overkill and I think that with associations, there has to be a better way.
Modification of the tables unfortunately is not an option.
One thing that I saw from "http://docs.sequelizejs.com/manual/querying.html#relations---associations"
I tried but got an interesting error.. when trying to repurpose the code snippet in the link under Relations/Associations, I get "user is associated to friends multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include."
const userRecord = await User.findOne({
where: { id }
})
const friendsIDs = await Friends.findAll({
attributes: ["friend_id"],
where: {
user_id: userRecord.id
}
}).then(results => results.map(result => result.friend_id));
const Sequelize = require("sequelize");
const Op = Sequelize.Op;
return await User.findAll({
where: {
id: { [Op.in]: friendsIDs }
},
});
Above for my use case works. I'm just wondering if there are ways to cut down the number of calls to the db.
Turns out Sequelize handles this for you if you have the proper associations in place so yes, it was a one liner user.getFriends() for me.

How to count association size with waterline/sails?

Using sails 0.10.5/waterline 0.10.15:
I cannot find an answer to a simple question: how to count the elements of an association without using populate() (which would load all data).
Let take a simple many2many relation with via:
User:
attributes: {
following: {
collection: 'user',
via: 'follower',
dominant: true
},
follower: {
collection: 'user',
via: 'following'
}
Now I need the size of the collections.
Currently I try
User.findById(1).populateAll().exec(function(err, user) {
// count of followings -> user.following.length;
// count of followers-> user.follower.length;
}
which leads to loading the collections.
I'm missing a count function at collection level to avoid population/loading of data.
Is there a possibility to access the (auto generated) join tables to run a count-query directly on the join?
Something like:
User.findById(1).count({'followings'}).exec(function(err, followings) {
...}
or
UserFollowingFollow_FollowFollowing.countByUserFollowingFollowId(1).
exec(function(err, followings) {
...}
Waterline does offer the count query method and it can be used like this to solve your problem:
User.count().where({follower: followerId})
.exec(function(err, numberOfFollowings) {
//numberOfFollowings will be the integer that you need
})
followerId is the id that you are passing to User.findOne() in your example.
You can also read the Waterline documentation about this.

MySQL many-to-many relationship in LoopBack

I'm having trouble getting LoopBack to perform a many-to-many join query. Considering the hasManyThrough example from the documentation:
var Physician = ds.createModel('Physician', {name: String});
var Patient = ds.createModel('Patient', {name: String});
var Appointment = ds.createModel('Appointment', {
physicianId: Number,
patientId: Number,
appointmentDate: Date
});
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
If I try to do a single search to find Patients associated with a particular doctor who have a zip code of 10012, I could try:
physician.patients({where: {zip: 10012}}, fn);
However, the search on the physician's patients is actually only searching on the Appointments table. Is there any way to do a simple search that will be performed on the specific physician's patients directly?
LoopBack implements the hasMany/through relation for physician.patients() as follows:
Appointment.find ({ where: { physicianId: 1 },
include: 'patient',
collect: 'patient' }, callback);
We're considering to support the filter for the 'include' which brings int 'patient' information.
I suggest you open an issue at https://github.com/strongloop/loopback-datasource-juggler/issues.