From b81b9283ad8a673382ccb9883ead8e5f6220cc5d Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Mon, 24 Jun 2024 13:11:51 +0530 Subject: [PATCH 1/6] Notification model --- .../Migrations/010_CreateNotifications.swift | 34 ++++++++++++++ Sources/App/Models/Notification.swift | 45 +++++++++++++++++++ Sources/App/configure.swift | 1 + 3 files changed, 80 insertions(+) create mode 100644 Sources/App/Migrations/010_CreateNotifications.swift create mode 100644 Sources/App/Models/Notification.swift diff --git a/Sources/App/Migrations/010_CreateNotifications.swift b/Sources/App/Migrations/010_CreateNotifications.swift new file mode 100644 index 0000000..d0f6909 --- /dev/null +++ b/Sources/App/Migrations/010_CreateNotifications.swift @@ -0,0 +1,34 @@ +// +// 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")) + .field("target_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 { + return try await database.schema("notifications").delete() + } +} diff --git a/Sources/App/Models/Notification.swift b/Sources/App/Models/Notification.swift new file mode 100644 index 0000000..df7da53 --- /dev/null +++ b/Sources/App/Models/Notification.swift @@ -0,0 +1,45 @@ +// +// 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? + + // 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/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() From 43a482382461d3fc9bf92364fe4618428bb031bc Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Wed, 26 Jun 2024 13:18:35 +0530 Subject: [PATCH 2/6] Notification graphql --- .../Relation/Notification+Resolver.swift | 24 +++++++++++++++++++ .../Relation/RegisteredUser+Resolver.swift | 4 ++++ Sources/App/GraphQL/Schema.swift | 16 +++++++++++++ Sources/App/Models/RegisteredUser.swift | 4 ++++ 4 files changed, 48 insertions(+) create mode 100644 Sources/App/GraphQL/Relation/Notification+Resolver.swift 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..b256ded 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) @@ -105,6 +112,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) 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) { From 67d6789530d1c42e278ab238faea2aa5359250ed Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Thu, 27 Jun 2024 11:10:14 +0530 Subject: [PATCH 3/6] Notification mutations --- .../Resolver+MutateNotifications.swift | 38 +++++++++++++++++++ .../Mutation/Resolver+MutatePosts.swift | 6 +++ .../Mutation/Resolver+MutateUser.swift | 3 ++ Sources/App/GraphQL/Schema.swift | 4 ++ Sources/App/Models/Notification.swift | 29 ++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 Sources/App/GraphQL/Mutation/Resolver+MutateNotifications.swift 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..c9ff3d8 100644 --- a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift +++ b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift @@ -38,6 +38,9 @@ extension Resolver { .first() else { throw Abort(.notFound, reason: "User \(arguments.id) not found") } + + let notif: Notification = .follow(targetUser: target, referenceUser: user) + try await notif.create(on: request.db) try await user.$following.attach(target, on: request.db) diff --git a/Sources/App/GraphQL/Schema.swift b/Sources/App/GraphQL/Schema.swift index b256ded..692614e 100644 --- a/Sources/App/GraphQL/Schema.swift +++ b/Sources/App/GraphQL/Schema.swift @@ -194,5 +194,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/Models/Notification.swift b/Sources/App/Models/Notification.swift index df7da53..2254c16 100644 --- a/Sources/App/Models/Notification.swift +++ b/Sources/App/Models/Notification.swift @@ -30,6 +30,35 @@ final class Notification: Model, Content { @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 From a67f200c03f1d35768b3f4a6d60157ec00da5de5 Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Thu, 27 Jun 2024 11:22:31 +0530 Subject: [PATCH 4/6] fix an oopsie --- Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift index c9ff3d8..6a7f6d4 100644 --- a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift +++ b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift @@ -39,7 +39,7 @@ extension Resolver { throw Abort(.notFound, reason: "User \(arguments.id) not found") } - let notif: Notification = .follow(targetUser: target, referenceUser: user) + let notif: Notification = .follow(targetUser: try target.requireID(), referenceUser: try user.requireID()) try await notif.create(on: request.db) try await user.$following.attach(target, on: request.db) From 0d13645925c52347d612053ccf84d5340884486a Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Thu, 27 Jun 2024 11:30:31 +0530 Subject: [PATCH 5/6] forgot the query lol --- Sources/App/GraphQL/Schema.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/App/GraphQL/Schema.swift b/Sources/App/GraphQL/Schema.swift index 692614e..754b3b8 100644 --- a/Sources/App/GraphQL/Schema.swift +++ b/Sources/App/GraphQL/Schema.swift @@ -62,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) { From 764652ee136d56dfd10f2184e6b27d40d564aefa Mon Sep 17 00:00:00 2001 From: Shrish Deshpande Date: Sat, 29 Jun 2024 15:58:07 +0530 Subject: [PATCH 6/6] Fix notification migration --- .../Mutation/Resolver+MutateUser.swift | 35 ++++++++++++++----- .../Migrations/010_CreateNotifications.swift | 8 +++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift b/Sources/App/GraphQL/Mutation/Resolver+MutateUser.swift index 6a7f6d4..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 } @@ -40,9 +45,16 @@ extension Resolver { } let notif: Notification = .follow(targetUser: try target.requireID(), referenceUser: try user.requireID()) - try await notif.create(on: request.db) - - try await user.$following.attach(target, on: request.db) + + 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() } @@ -53,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/Migrations/010_CreateNotifications.swift b/Sources/App/Migrations/010_CreateNotifications.swift index d0f6909..abe1c43 100644 --- a/Sources/App/Migrations/010_CreateNotifications.swift +++ b/Sources/App/Migrations/010_CreateNotifications.swift @@ -18,8 +18,9 @@ struct CreateNotifications: AsyncMigration { .create() try await db.schema("notifications") .field("id", .string, .required) - .field("target_user_id", .int, .references("registeredUsers", "id")) - .field("target_post_id", .string, .references("posts", "id")) + .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) @@ -29,6 +30,7 @@ struct CreateNotifications: AsyncMigration { } func revert(on database: Database) async throws { - return try await database.schema("notifications").delete() + try await database.schema("notifications").delete() + try await database.enum("notificaion_type").delete() } }