diff --git a/Sources/App/GraphQL/Mutation/Resolver+MutateNotifications.swift b/Sources/App/GraphQL/Mutation/Resolver+MutateNotifications.swift new file mode 100644 index 0000000..59ef08d --- /dev/null +++ b/Sources/App/GraphQL/Mutation/Resolver+MutateNotifications.swift @@ -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 + } +} diff --git a/Sources/App/GraphQL/Mutation/Resolver+MutatePosts.swift b/Sources/App/GraphQL/Mutation/Resolver+MutatePosts.swift index 3690810..e5bbcc5 100644 --- a/Sources/App/GraphQL/Mutation/Resolver+MutatePosts.swift +++ b/Sources/App/GraphQL/Mutation/Resolver+MutatePosts.swift @@ -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() } diff --git a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift index 22c90e5..1324086 100644 --- a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift +++ b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift @@ -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 } @@ -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() } @@ -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() } diff --git a/Sources/App/GraphQL/Relation/Notification+Resolver.swift b/Sources/App/GraphQL/Relation/Notification+Resolver.swift new file mode 100644 index 0000000..b6c87ca --- /dev/null +++ b/Sources/App/GraphQL/Relation/Notification+Resolver.swift @@ -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) + } +} diff --git a/Sources/App/GraphQL/Relation/RegisteredUser+Resolver.swift b/Sources/App/GraphQL/Relation/RegisteredUser+Resolver.swift index 8a70f4f..3c65112 100644 --- a/Sources/App/GraphQL/Relation/RegisteredUser+Resolver.swift +++ b/Sources/App/GraphQL/Relation/RegisteredUser+Resolver.swift @@ -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() + } } diff --git a/Sources/App/GraphQL/Schema.swift b/Sources/App/GraphQL/Schema.swift index 01df712..754b3b8 100644 --- a/Sources/App/GraphQL/Schema.swift +++ b/Sources/App/GraphQL/Schema.swift @@ -16,6 +16,13 @@ let schema = try! Graphiti.Schema { 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) @@ -55,6 +62,7 @@ let schema = try! Graphiti.Schema { Field("followedBySelf", at: RegisteredUser.followedBySelf) Field("followsSelf", at: RegisteredUser.followsSelf) Field("avatarHash", at: \.avatarHash) + Field("notifications", at: RegisteredUser.getNotifications) } Type(Post.self) { @@ -105,6 +113,15 @@ let schema = try! Graphiti.Schema { 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) @@ -178,5 +195,9 @@ let schema = try! Graphiti.Schema { Field("unlikeConfession", at: Resolver.unlikeConfession) { Argument("id", at: \.id) } + Field("readNotification", at: Resolver.readNotification) { + Argument("id", at: \.id) + } + Field("readAllNotifications", at: Resolver.readAllNotifications) } } diff --git a/Sources/App/Migrations/010_CreateNotifications.swift b/Sources/App/Migrations/010_CreateNotifications.swift new file mode 100644 index 0000000..abe1c43 --- /dev/null +++ b/Sources/App/Migrations/010_CreateNotifications.swift @@ -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() + } +} diff --git a/Sources/App/Models/Notification.swift b/Sources/App/Models/Notification.swift new file mode 100644 index 0000000..2254c16 --- /dev/null +++ b/Sources/App/Models/Notification.swift @@ -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 + } +} diff --git a/Sources/App/Models/RegisteredUser.swift b/Sources/App/Models/RegisteredUser.swift index 39b9306..bf3a8d9 100644 --- a/Sources/App/Models/RegisteredUser.swift +++ b/Sources/App/Models/RegisteredUser.swift @@ -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) { diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index af8a009..068e88b 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -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()