Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ type Query {
getUserFriends(userId: Int!): [User]
getCapacityReminderById(id: Int!): CapacityReminder
getAllCapacityReminders: [CapacityReminder]
searchFriend(searchTerm: String!): [User]
}

type RefreshAccessToken {
Expand Down Expand Up @@ -316,6 +317,7 @@ type User {
friendRequestsReceived: [Friendship]
friendships: [Friendship]
friends: [User]
friendStatusWithCurrentUser: String
}

type Workout {
Expand Down
104 changes: 104 additions & 0 deletions src/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import os
from firebase_admin import messaging
import logging
from sqlalchemy import or_, and_


def resolve_enum_value(entry):
Expand Down Expand Up @@ -204,6 +205,7 @@ class Meta:
workout_goal = graphene.List(DayOfWeekGraphQLEnum)
friendships = graphene.List(lambda: Friendship)
friends = graphene.List(lambda: User)
friend_status_with_current_user = graphene.String(name="friendStatusWithCurrentUser")

def resolve_friendships(self, info):
# Return all friendship relationships for this user
Expand Down Expand Up @@ -231,6 +233,38 @@ def resolve_friends(self, info):
# Query for all the users at once
return User.get_query(info).filter(UserModel.id.in_(friend_ids)).all()

def resolve_friend_status_with_current_user(self, info):
# Prefer precomputed map from parent resolver to avoid N+1 queries
status_map = getattr(info.context, "friend_status_map", None)
if status_map is not None:
return status_map.get(self.id, "none")

# If no context map, fall back to checking a single friendship if a JWT is present
try:
current_user_id = int(get_jwt_identity())
except Exception:
return None

if not current_user_id:
return None

fs = (
Friendship.get_query(info)
.filter(
or_(
and_(FriendshipModel.user_id == current_user_id, FriendshipModel.friend_id == self.id),
and_(FriendshipModel.friend_id == current_user_id, FriendshipModel.user_id == self.id),
)
)
.first()
)

if not fs:
return "none"
if fs.is_accepted:
return "friends"
return "pending_outgoing" if fs.user_id == current_user_id else "pending_incoming"


class UserInput(graphene.InputObjectType):
net_id = graphene.String(required=True)
Expand Down Expand Up @@ -340,6 +374,11 @@ class Query(graphene.ObjectType):
CapacityReminder,
description="Get all capacity reminders."
)
search_friend = graphene.List(
User,
search_term=graphene.String(required=True),
description="Search for users by name or NetID."
)

def resolve_get_all_gyms(self, info):
query = Gym.get_query(info)
Expand Down Expand Up @@ -496,7 +535,51 @@ def resolve_get_capacity_reminder_by_id(self, info, id):
def resolve_get_all_capacity_reminders(self, info):
query = CapacityReminder.get_query(info)
return query.all()

@jwt_required()
def resolve_search_friend(self, info, search_term):
query = User.get_query(info)
search = f"{search_term}%"

current_user_id = int(get_jwt_identity())

users = query.filter(
or_(
UserModel.name.ilike(search),
UserModel.net_id.ilike(search)
),
UserModel.id != current_user_id,
).all()

candidate_ids = [user.id for user in users]
friend_status_map = {}

if candidate_ids:
friendships = Friendship.get_query(info).filter(
or_(
and_(FriendshipModel.user_id == current_user_id, FriendshipModel.friend_id.in_(candidate_ids)),
and_(FriendshipModel.friend_id == current_user_id, FriendshipModel.user_id.in_(candidate_ids)),
)
).all()

for fs in friendships:
other_id = fs.friend_id if fs.user_id == current_user_id else fs.user_id
if fs.is_accepted:
status = "friends"
elif fs.user_id == current_user_id:
status = "pending_outgoing"
else:
status = "pending_incoming"
friend_status_map[other_id] = status

# Stash for the User.friendStatusWithCurrentUser resolver to avoid N+1 queries
setattr(info.context, "friend_status_map", friend_status_map)

# Provide default status when the resolver is not invoked (e.g., unused field)
for user in users:
user.friend_status_with_current_user = friend_status_map.get(user.id, "none")

return users

# MARK: - Mutation

Expand Down Expand Up @@ -830,6 +913,14 @@ def mutate(self, info, user_id):
user = User.get_query(info).filter(UserModel.id == user_id).first()
if not user:
raise GraphQLError("User with given ID does not exist.")

# Remove any friendships that reference this user to avoid orphaned rows
friendships = Friendship.get_query(info).filter(
or_(FriendshipModel.user_id == user_id, FriendshipModel.friend_id == user_id)
)
for friendship in friendships:
db_session.delete(friendship)

db_session.delete(user)
db_session.commit()
return user
Expand Down Expand Up @@ -1009,6 +1100,9 @@ class Arguments:

@jwt_required()
def mutate(self, info, user_id, friend_id):
if user_id == friend_id:
raise GraphQLError("You cannot add yourself as a friend.")

# Check if users exist
user = User.get_query(info).filter(UserModel.id == user_id).first()
if not user:
Expand All @@ -1018,6 +1112,16 @@ def mutate(self, info, user_id, friend_id):
if not friend:
raise GraphQLError("Friend with given ID does not exist.")

# If a pending request exists in the opposite direction, auto-accept it
reverse_existing = Friendship.get_query(info).filter(
(FriendshipModel.user_id == friend_id) & (FriendshipModel.friend_id == user_id)
).first()
if reverse_existing and not reverse_existing.is_accepted:
reverse_existing.is_accepted = True
reverse_existing.accepted_at = datetime.utcnow()
db_session.commit()
return reverse_existing

# Check if friendship already exists
existing = Friendship.get_query(info).filter(
((FriendshipModel.user_id == user_id) & (FriendshipModel.friend_id == friend_id)) |
Expand Down
Loading