diff --git a/apps/web/dao/user.dao.test.ts b/apps/web/dao/user.dao.test.ts new file mode 100644 index 000000000..c4352237d --- /dev/null +++ b/apps/web/dao/user.dao.test.ts @@ -0,0 +1,72 @@ +import { UserDao } from "./user.dao"; +import UserModel from "@models/User"; +import mongoose from "mongoose"; + +afterEach(async () => { + await UserModel.deleteMany({}); +}); + +describe("UserDao", () => { + let userDao: UserDao; + + beforeEach(() => { + userDao = new UserDao(); + }); + + it("should create a user", async () => { + const userData = { + email: "test@example.com", + name: "Test User", + userId: "123", + unsubscribeToken: "token", + domain: new mongoose.Types.ObjectId().toString(), + }; + const user = await userDao.createUser(userData); + expect(user).toBeDefined(); + expect(user.email).toBe(userData.email); + }); + + it("should get a user by id", async () => { + const userData = { + email: "test@example.com", + name: "Test User", + userId: "123", + unsubscribeToken: "token", + domain: new mongoose.Types.ObjectId().toString(), + }; + await userDao.createUser(userData); + const user = await userDao.getUserById("123", userData.domain); + expect(user).toBeDefined(); + expect(user!.email).toBe(userData.email); + }); + + it("should get a user by email", async () => { + const userData = { + email: "test@example.com", + name: "Test User", + userId: "123", + unsubscribeToken: "token", + domain: new mongoose.Types.ObjectId().toString(), + }; + await userDao.createUser(userData); + const user = await userDao.getUserByEmail("test@example.com", userData.domain); + expect(user).toBeDefined(); + expect(user!.email).toBe(userData.email); + }); + + it("should update a user", async () => { + const userData = { + email: "test@example.com", + name: "Test User", + userId: "123", + unsubscribeToken: "token", + domain: new mongoose.Types.ObjectId().toString(), + }; + await userDao.createUser(userData); + const updatedUser = await userDao.updateUser("123", userData.domain, { + name: "Updated Test User", + }); + expect(updatedUser).toBeDefined(); + expect(updatedUser!.name).toBe("Updated Test User"); + }); +}); diff --git a/apps/web/dao/user.dao.ts b/apps/web/dao/user.dao.ts new file mode 100644 index 000000000..db56c7139 --- /dev/null +++ b/apps/web/dao/user.dao.ts @@ -0,0 +1,155 @@ +import { User } from "@courselit/common-models"; +import UserModel from "@models/User"; +import { makeModelTextSearchable } from "@/lib/graphql"; + +export interface IUserDao { + getUserById(userId: string, domainId: string): Promise; + getUserByEmail(email: string, domainId: string): Promise; + createUser(userData: Partial): Promise; + updateUser( + userId: string, + domainId: string, + userData: Partial, + ): Promise; + find(query: any): Promise; + findOne(query: any): Promise; + findOneAndUpdate( + query: any, + update: any, + options: any, + ): Promise; + countDocuments(query: any): Promise; + updateMany(query: any, update: any): Promise; + aggregate(pipeline: any[]): Promise; + deleteUser(userId: string, domainId: string): Promise; + search( + { + offset, + query, + graphQLContext, + }: { offset: number; query: any; graphQLContext: any }, + { + itemsPerPage, + sortByColumn, + sortOrder, + }: { + itemsPerPage: number; + sortByColumn: string; + sortOrder: number; + }, + ): Promise; +} + +export class UserDao implements IUserDao { + public async getUserById( + userId: string, + domainId: string, + ): Promise { + const user = await UserModel.findOne({ + userId, + domain: domainId, + }).lean(); + return user as User; + } + + public async getUserByEmail( + email: string, + domainId: string, + ): Promise { + const user = await UserModel.findOne({ + email, + domain: domainId, + }).lean(); + return user as User; + } + + public async createUser(userData: Partial): Promise { + const user = new UserModel(userData); + const newUser = await user.save(); + return newUser.toObject(); + } + + public async updateUser( + userId: string, + domainId: string, + userData: Partial, + ): Promise { + const user = await UserModel.findOneAndUpdate( + { userId, domain: domainId }, + { $set: userData }, + { new: true }, + ).lean(); + return user as User; + } + + public async find(query: any): Promise { + const users = await UserModel.find(query).lean(); + return users as User[]; + } + + public async findOne(query: any): Promise { + const user = await UserModel.findOne(query).lean(); + return user as User; + } + + public async findOneAndUpdate( + query: any, + update: any, + options: any, + ): Promise { + const user = await UserModel.findOneAndUpdate( + query, + update, + options, + ).lean(); + return user as User; + } + + public async countDocuments(query: any): Promise { + return await UserModel.countDocuments(query); + } + + public async updateMany(query: any, update: any): Promise { + return await UserModel.updateMany(query, update); + } + + public async aggregate(pipeline: any[]): Promise { + return await UserModel.aggregate(pipeline); + } + + public async deleteUser(userId: string, domainId: string): Promise { + return await UserModel.deleteOne({ userId, domain: domainId }); + } + + public async search( + { + offset, + query, + graphQLContext, + }: { offset: number; query: any; graphQLContext: any }, + { + itemsPerPage, + sortByColumn, + sortOrder, + }: { + itemsPerPage: number; + sortByColumn: string; + sortOrder: number; + }, + ): Promise { + const searchUsers = makeModelTextSearchable(UserModel); + const users = await searchUsers( + { + offset, + query, + graphQLContext, + }, + { + itemsPerPage, + sortByColumn, + sortOrder, + }, + ); + return users.map((user: any) => user.toObject()); + } +} diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts index 843d1305d..2a0883633 100644 --- a/apps/web/graphql/users/logic.ts +++ b/apps/web/graphql/users/logic.ts @@ -1,9 +1,11 @@ "use server"; -import UserModel from "@models/User"; import { responses } from "@/config/strings"; -import { makeModelTextSearchable, checkIfAuthenticated } from "@/lib/graphql"; +import { checkIfAuthenticated } from "@/lib/graphql"; +import { UserDao } from "@/dao/user.dao"; import constants from "@/config/constants"; + +const userDao = new UserDao(); import GQLContext from "@/models/GQLContext"; import { initMandatoryPages } from "../pages/logic"; import { Domain } from "@models/Domain"; @@ -73,7 +75,7 @@ export const getUser = async (userId = null, ctx: GQLContext) => { let user: any = ctx.user; if (userId) { - user = await UserModel.findOne({ userId, domain: ctx.subdomain._id }); + user = await userDao.findOne({ userId, domain: ctx.subdomain._id }); } if (!user) { @@ -124,25 +126,26 @@ export const updateUser = async (userData: UserData, ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - let user = await UserModel.findOne({ + let user = await userDao.findOne({ userId: id, domain: ctx.subdomain._id, }); if (!user) throw new Error(responses.item_not_found); + const updateData: Partial = {}; for (const key of keys.filter((key) => key !== "id")) { if (key === "tags") { - addTags(userData["tags"]!, ctx); + await addTags(userData["tags"]!, ctx); } - user[key] = userData[key]; + (updateData as any)[key] = (userData as any)[key]; } - validateUserProperties(user); + validateUserProperties(updateData); - user = await user.save(); + const updatedUser = await userDao.updateUser(id, ctx.subdomain._id as any, updateData); - return user; + return updatedUser; }; export const inviteCustomer = async ( @@ -162,7 +165,7 @@ export const inviteCustomer = async ( } const sanitizedEmail = (email as string).toLowerCase(); - let user = await UserModel.findOne({ + let user = await userDao.findOne({ email: sanitizedEmail, domain: ctx.subdomain._id, }); @@ -232,7 +235,7 @@ export const deleteUser = async ( throw new Error(responses.action_not_allowed); } - const userToDelete = await UserModel.findOne({ + const userToDelete = await userDao.findOne({ domain: ctx.subdomain._id, userId, }); @@ -246,16 +249,22 @@ export const deleteUser = async ( } const deleterUser = - (await UserModel.findOne({ + (await userDao.findOne({ domain: ctx.subdomain._id, userId: ctx.user.userId, })) || (ctx.user as InternalUser); - await validateUserDeletion(userToDelete, ctx); + await validateUserDeletion(userToDelete as InternalUser, ctx); + + await migrateBusinessEntities( + userToDelete as InternalUser, + deleterUser as InternalUser, + ctx, + ); - await migrateBusinessEntities(userToDelete, deleterUser, ctx); + await cleanupPersonalData(userToDelete as InternalUser, ctx); - await cleanupPersonalData(userToDelete, ctx); + await userDao.deleteUser(userId, ctx.subdomain._id as any); return true; }; @@ -278,9 +287,8 @@ export const getUsers = async ({ throw new Error(responses.action_not_allowed); } - const searchUsers = makeModelTextSearchable(UserModel); const query = await buildQueryFromSearchData(ctx.subdomain._id, filters); - const users = await searchUsers( + const users = await userDao.search( { offset: page, query, @@ -306,7 +314,7 @@ export const getUsersCount = async (ctx: GQLContext, filters?: string) => { } const query = await buildQueryFromSearchData(ctx.subdomain._id, filters); - return await UserModel.countDocuments(query); + return await userDao.countDocuments(query); }; const buildQueryFromSearchData = async ( @@ -379,7 +387,7 @@ export async function createUser({ checkForInvalidPermissions(permissions); } - const rawResult = await UserModel.findOneAndUpdate( + const rawResult = await userDao.findOneAndUpdate( { domain: domain._id, email }, { $setOnInsert: { @@ -529,7 +537,7 @@ export const getTagsWithDetails = async (ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - const tagsWithUsersCount = await UserModel.aggregate([ + const tagsWithUsersCount = await userDao.aggregate([ { $unwind: "$tags" }, { $match: { @@ -603,7 +611,7 @@ export const deleteTag = async (tag: string, ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - await UserModel.updateMany( + await userDao.updateMany( { domain: ctx.subdomain._id }, { $pull: { tags: tag } }, ); @@ -622,7 +630,7 @@ export const untagUsers = async (tag: string, ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - await UserModel.updateMany( + await userDao.updateMany( { domain: ctx.subdomain._id }, { $pull: { tags: tag } }, ); @@ -644,7 +652,7 @@ export const getUserContent = async ( id = userId; } - const user = await UserModel.findOne({ + const user = await userDao.findOne({ userId: id, domain: ctx.subdomain._id, }); @@ -813,7 +821,7 @@ export async function runPostMembershipTasks({ membership: Membership; paymentPlan: PaymentPlan; }) { - const user = await UserModel.findOne({ + const user = await userDao.findOne({ userId: membership.userId, }); if (!user) { @@ -930,10 +938,10 @@ export const getCertificateInternal = async ( const user = certificateId !== "demo" - ? ((await UserModel.findOne({ + ? ((await userDao.findOne({ domain: domain._id, userId: certificate.userId, - }).lean()) as unknown as User) + })) as unknown as User) : { name: "John Doe", email: "john.doe@example.com", @@ -949,10 +957,10 @@ export const getCertificateInternal = async ( throw new Error(responses.item_not_found); } - const creator = (await UserModel.findOne({ + const creator = (await userDao.findOne({ domain: domain._id, userId: course.creatorId, - }).lean()) as unknown as User; + })) as unknown as User; const template = (await CertificateTemplateModel.findOne({ domain: domain._id, diff --git a/apps/web/jest.server.config.ts b/apps/web/jest.server.config.ts index d3b9cc17c..1484d838e 100644 --- a/apps/web/jest.server.config.ts +++ b/apps/web/jest.server.config.ts @@ -20,6 +20,7 @@ const config: Config = { "@/lib/(.*)": "/lib/$1", "@/services/(.*)": "/services/$1", "@/templates/(.*)": "/templates/$1", + "@/dao/(.*)": "/dao/$1", "@/app/(.*)": "/app/$1", "@ui-lib/(.*)": "/ui-lib/$1", "@config/(.*)": "/config/$1", @@ -39,6 +40,7 @@ const config: Config = { testMatch: [ "**/graphql/**/__tests__/**/*.test.ts", "**/api/**/__tests__/**/*.test.ts", + "**/dao/**/*.test.ts", ], testPathIgnorePatterns: [ "/node_modules/",