Skip to content
Merged
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
38 changes: 38 additions & 0 deletions Sources/App/GraphQL/Mutation/Resolver+MutateNotifications.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Resolver+MutateNotifications.swift
//
//
// Created by Shrish Deshpande on 27/06/24.
//

import Vapor
import Fluent
import Graphiti

extension Resolver {
func readAllNotifications(request: Request, arguments: NoArguments) async throws -> Int {
try await assertScope(request: request, .editProfile)
let user = try await getContextUser(request)
let notifications = try await user.$notifications.query(on: request.db).all()
let count = try await request.db.transaction { db in
var ct = 0
for notif in notifications {
try await notif.delete(on: db)
ct += 1
}
return ct
}
return count
}

func readNotification(request: Request, arguments: StringIdArgs) async throws -> Bool {
try await assertScope(request: request, .editProfile)
let user = try await getContextUser(request)
let notification = try await user.$notifications.query(on: request.db).filter(\.$id == arguments.id).first()
guard let notif = notification else {
throw Abort(.notFound, reason: "Notification not found")
}
try await notif.delete(on: request.db)
return true
}
}
6 changes: 6 additions & 0 deletions Sources/App/GraphQL/Mutation/Resolver+MutatePosts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,13 @@ extension Resolver {
try await assertScope(request: request, .createPosts)
let token = try await getAndVerifyAccessToken(req: request)
let lp: LikedPost = .init(postId: arguments.id, userId: token.id)
let post: Post? = try await Post.find(arguments.id, on: request.db)
guard let post = post else {
throw Abort(.notFound, reason: "Could not find post with given ID")
}
try await lp.create(on: request.db)
let notif: Notification = .like(target: post.$creator.id, user: token.id, post: arguments.id)
try await notif.create(on: request.db)
return try await LikedPost.query(on: request.db).filter(\.$post.$id == arguments.id).count().get()
}

Expand Down
36 changes: 28 additions & 8 deletions Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ extension Resolver {
user.setValue(\.bio, arguments.bio, orElse: nil)
user.setValue(\.pronouns, arguments.pronouns, orElse: nil)

try await user.update(on: request.db)
do {
try await user.update(on: request.db)
} catch {
request.logger.error("Error updating user profile: \(String(reflecting: error))")
throw Abort(.internalServerError, reason: "Error updating user profile")
}

return user
}
Expand All @@ -38,8 +43,18 @@ extension Resolver {
.first() else {
throw Abort(.notFound, reason: "User \(arguments.id) not found")
}

try await user.$following.attach(target, on: request.db)

let notif: Notification = .follow(targetUser: try target.requireID(), referenceUser: try user.requireID())

do {
try await request.db.transaction { db in
try await notif.create(on: db)
try await user.$following.attach(target, on: db)
}
} catch {
request.logger.error("Error following user: \(String(reflecting: error))")
throw Abort(.internalServerError, reason: "Error following user")
}

return try await target.$following.query(on: request.db).count()
}
Expand All @@ -50,12 +65,17 @@ extension Resolver {
let user = try await getContextUser(request)

let target = try await RegisteredUser.query(on: request.db)
.filter(\.$id == arguments.id)
.first()
.unwrap(or: Abort(.notFound, reason: "User \(arguments.id) not found"))
.get()
.filter(\.$id == arguments.id)
.first()
.unwrap(or: Abort(.notFound, reason: "User \(arguments.id) not found"))
.get()

try await user.$following.detach(target, on: request.db)
do {
try await user.$following.detach(target, on: request.db)
} catch {
request.logger.error("Error unfollowing user: \(String(reflecting: error))")
throw Abort(.internalServerError, reason: "Error unfollowing user")
}

return try await target.$following.query(on: request.db).count()
}
Expand Down
24 changes: 24 additions & 0 deletions Sources/App/GraphQL/Relation/Notification+Resolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Notification+Resolver.swift
//
//
// Created by Shrish Deshpande on 24/06/24.
//

import Vapor
import Fluent
import Graphiti

extension Notification {
func getTargetUser(request: Request, arguments: NoArguments) async throws -> RegisteredUser {
return try await self.$targetUser.get(on: request.db)
}

func getReferenceUser(request: Request, arguments: NoArguments) async throws -> RegisteredUser? {
return try await self.$referenceUser.get(on: request.db)
}

func getReferencePost(request: Request, arguments: NoArguments) async throws -> Post? {
return try await self.$referencePost.get(on: request.db)
}
}
4 changes: 4 additions & 0 deletions Sources/App/GraphQL/Relation/RegisteredUser+Resolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ extension RegisteredUser {
let token = try await getAndVerifyAccessToken(req: request)
return try await self.$followers.isAttached(toID: token.id, on: request.db)
}

func getNotifications(request: Request, arguments: NoArguments) async throws -> [Notification] {
return try await self.$notifications.query(on: request.db).all()
}
}
21 changes: 21 additions & 0 deletions Sources/App/GraphQL/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ let schema = try! Graphiti.Schema<Resolver, Request> {
Scalar(UUID.self)
Scalar(Date.self)

Enum(Notification.NotificationType.self) {
Value(.follow)
Value(.like)
Value(.comment)
Value(.mention)
}

Type(UnregisteredUser.self) {
Field("collegeId", at: \.id)
Field("name", at: \.name)
Expand Down Expand Up @@ -55,6 +62,7 @@ let schema = try! Graphiti.Schema<Resolver, Request> {
Field("followedBySelf", at: RegisteredUser.followedBySelf)
Field("followsSelf", at: RegisteredUser.followsSelf)
Field("avatarHash", at: \.avatarHash)
Field("notifications", at: RegisteredUser.getNotifications)
}

Type(Post.self) {
Expand Down Expand Up @@ -105,6 +113,15 @@ let schema = try! Graphiti.Schema<Resolver, Request> {
Field("items", at: \.items)
Field("metadata", at: \.metadata)
}

Type(Notification.self) {
Field("id", at : \.id)
Field("targetUser", at: Notification.getTargetUser)
Field("referenceUser", at: Notification.getReferenceUser)
Field("referencePost", at: Notification.getReferencePost)
Field("createdAt", at: \.createdAt?.timeIntervalSince1970)
Field("type", at: \.type)
}

Query {
Field("users", at: Resolver.getAllRegisteredUsers)
Expand Down Expand Up @@ -178,5 +195,9 @@ let schema = try! Graphiti.Schema<Resolver, Request> {
Field("unlikeConfession", at: Resolver.unlikeConfession) {
Argument("id", at: \.id)
}
Field("readNotification", at: Resolver.readNotification) {
Argument("id", at: \.id)
}
Field("readAllNotifications", at: Resolver.readAllNotifications)
}
}
36 changes: 36 additions & 0 deletions Sources/App/Migrations/010_CreateNotifications.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 009_CreateAttachments.swift
//
//
// Created by Shrish Deshpande on 19/06/24.
//

import Fluent

struct CreateNotifications: AsyncMigration {
func prepare(on database: Database) async throws {
return try await database.transaction { db in
let type = try await db.enum("notification_type")
.case("follow")
.case("like")
.case("comment")
.case("mention")
.create()
try await db.schema("notifications")
.field("id", .string, .required)
.field("target_user_id", .int, .references("registeredUsers", "id"), .required)
.field("reference_user_id", .int, .references("registeredUsers", "id"))
.field("reference_post_id", .string, .references("posts", "id"))
.field("created_at", .datetime, .required)
.field("deleted_at", .datetime)
.field("type", type, .required)
.unique(on: "id")
.create()
}
}

func revert(on database: Database) async throws {
try await database.schema("notifications").delete()
try await database.enum("notificaion_type").delete()
}
}
74 changes: 74 additions & 0 deletions Sources/App/Models/Notification.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Notification.swift
//
//
// Created by Shrish Deshpande on 24/06/24.
//

import Vapor
import Fluent
import Foundation

final class Notification: Model, Content {
public static let schema = "notifications"

@ID(custom: "id", generatedBy: .user)
var id: String?

@Parent(key: "target_user_id")
var targetUser: RegisteredUser

@Timestamp(key: "created_at", on: .create)
var createdAt: Date?

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

@OptionalParent(key: "reference_post_id")
var referencePost: Post?

@OptionalParent(key: "reference_user_id")
var referenceUser: RegisteredUser?

public init() {
}

private static func create(type: NotificationType, targetUser: Int, referenceUser: Int? = nil, referencePost: String? = nil) -> Notification {
let notif = Notification()
notif.id = Snowflake.init().stringValue
notif.$targetUser.id = targetUser
notif.$referenceUser.id = referenceUser
notif.$referencePost.id = referencePost
notif.type = type
return notif
}

public static func follow(targetUser: Int, referenceUser: Int) -> Notification {
return create(type: .follow, targetUser: targetUser, referenceUser: referenceUser)
}

public static func like(target: Int, user: Int, post: String) -> Notification {
return create(type: .like, targetUser: target, referenceUser: user, referencePost: post)
}

public static func comment(targetUser: Int, referenceUser: Int, referencePost: String) -> Notification {
return create(type: .comment, targetUser: targetUser, referenceUser: referenceUser, referencePost: referencePost)
}

public static func mention(targetUser: Int, referenceUser: Int, referencePost: String) -> Notification {
return create(type: .mention, targetUser: targetUser, referenceUser: referenceUser, referencePost: referencePost)
}

// TODO: add comment reference
// TODO: add confession reference

@Enum(key: "type")
var type: NotificationType

enum NotificationType: String, Codable {
case follow
case like
case comment
case mention
}
}
4 changes: 4 additions & 0 deletions Sources/App/Models/RegisteredUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ public final class RegisteredUser: Model, Content {
@Siblings(through: LikedConfession.self, from: \.$user, to: \.$confession)
var likedConfessions: [Confession]

/// List of notifications
@Children(for: \.$targetUser)
var notifications: [Notification]

public init() { }

public init(collegeId: String, name: String, phone: String, email: String, personalEmail: String? = nil, branch: String, gender: String, pronouns: String? = nil, bio: String? = nil, intakeYear: Int, id: Int? = nil) {
Expand Down
1 change: 1 addition & 0 deletions Sources/App/configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public func configure(_ app: Application) async throws {
app.migrations.add(CreateConfessions())
app.migrations.add(CreateLikedConfessions())
app.migrations.add(CreateAttachments())
app.migrations.add(CreateNotifications())

appConfig = AppConfig.firstLoad()

Expand Down