From 408bc62501ecbb2a8d7e9b09403eee09d95952ca Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 11 Jul 2026 06:36:07 +0000 Subject: [PATCH 1/3] feat: implement repository pattern for data access (#822) --- packages/orm-models/src/index.ts | 14 +++++++ packages/orm-models/src/models/media.ts | 2 +- packages/orm-models/src/repositories/base.ts | 41 +++++++++++++++++++ .../src/repositories/community-comment.ts | 27 ++++++++++++ .../src/repositories/community-post.ts | 34 +++++++++++++++ .../orm-models/src/repositories/community.ts | 30 ++++++++++++++ .../orm-models/src/repositories/course.ts | 36 ++++++++++++++++ .../orm-models/src/repositories/domain.ts | 21 ++++++++++ .../orm-models/src/repositories/lesson.ts | 30 ++++++++++++++ packages/orm-models/src/repositories/media.ts | 22 ++++++++++ .../orm-models/src/repositories/membership.ts | 34 +++++++++++++++ .../src/repositories/notification.ts | 26 ++++++++++++ packages/orm-models/src/repositories/page.ts | 20 +++++++++ .../src/repositories/payment-plan.ts | 33 +++++++++++++++ .../orm-models/src/repositories/site-info.ts | 19 +++++++++ packages/orm-models/src/repositories/user.ts | 27 ++++++++++++ 16 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 packages/orm-models/src/repositories/base.ts create mode 100644 packages/orm-models/src/repositories/community-comment.ts create mode 100644 packages/orm-models/src/repositories/community-post.ts create mode 100644 packages/orm-models/src/repositories/community.ts create mode 100644 packages/orm-models/src/repositories/course.ts create mode 100644 packages/orm-models/src/repositories/domain.ts create mode 100644 packages/orm-models/src/repositories/lesson.ts create mode 100644 packages/orm-models/src/repositories/media.ts create mode 100644 packages/orm-models/src/repositories/membership.ts create mode 100644 packages/orm-models/src/repositories/notification.ts create mode 100644 packages/orm-models/src/repositories/page.ts create mode 100644 packages/orm-models/src/repositories/payment-plan.ts create mode 100644 packages/orm-models/src/repositories/site-info.ts create mode 100644 packages/orm-models/src/repositories/user.ts diff --git a/packages/orm-models/src/index.ts b/packages/orm-models/src/index.ts index 70c2a3b0d..b6b829afd 100644 --- a/packages/orm-models/src/index.ts +++ b/packages/orm-models/src/index.ts @@ -38,3 +38,17 @@ export * from "./models/user-theme"; export * from "./models/product-discussion"; export * from "./models/rate-limit-event"; export * from "./product-discussion-cleanup"; +export * from "./repositories/base"; +export * from "./repositories/user"; +export * from "./repositories/course"; +export * from "./repositories/domain"; +export * from "./repositories/community"; +export * from "./repositories/community-post"; +export * from "./repositories/community-comment"; +export * from "./repositories/lesson"; +export * from "./repositories/media"; +export * from "./repositories/membership"; +export * from "./repositories/notification"; +export * from "./repositories/payment-plan"; +export * from "./repositories/page"; +export * from "./repositories/site-info"; diff --git a/packages/orm-models/src/models/media.ts b/packages/orm-models/src/models/media.ts index bb50e1dd2..f5082d8a4 100644 --- a/packages/orm-models/src/models/media.ts +++ b/packages/orm-models/src/models/media.ts @@ -1,7 +1,7 @@ import { Constants, Media } from "@courselit/common-models"; import mongoose from "mongoose"; -type MediaWithOwner = Media & { userId: string }; +export type MediaWithOwner = Media & { userId: string }; export const MediaSchema = new mongoose.Schema({ mediaId: { type: String, required: true }, diff --git a/packages/orm-models/src/repositories/base.ts b/packages/orm-models/src/repositories/base.ts new file mode 100644 index 000000000..fdb185841 --- /dev/null +++ b/packages/orm-models/src/repositories/base.ts @@ -0,0 +1,41 @@ +import { + Model, + type FilterQuery, + type UpdateQuery, + type QueryOptions, +} from "mongoose"; + +export class BaseRepository { + constructor(protected model: Model) {} + + async findById(id: string): Promise { + return this.model.findById(id).exec(); + } + + async findOne(filter: FilterQuery): Promise { + return this.model.findOne(filter).exec(); + } + + async find( + filter: FilterQuery = {}, + options?: QueryOptions, + ): Promise { + return this.model.find(filter, null, options).exec(); + } + + async create(data: Partial): Promise { + return this.model.create(data); + } + + async update(id: string, data: UpdateQuery): Promise { + return this.model.findByIdAndUpdate(id, data, { new: true }).exec(); + } + + async delete(id: string): Promise { + return this.model.findByIdAndDelete(id).exec(); + } + + async count(filter: FilterQuery = {}): Promise { + return this.model.countDocuments(filter).exec(); + } +} diff --git a/packages/orm-models/src/repositories/community-comment.ts b/packages/orm-models/src/repositories/community-comment.ts new file mode 100644 index 000000000..b49336b56 --- /dev/null +++ b/packages/orm-models/src/repositories/community-comment.ts @@ -0,0 +1,27 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { + CommunityCommentSchema, + type InternalCommunityComment, +} from "../models/community-comment"; + +export class CommunityCommentRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.CommunityComment || + mongoose.model( + "CommunityComment", + CommunityCommentSchema, + )) as Model), + ); + } + + async findByPost( + domain: mongoose.Types.ObjectId, + communityId: string, + postId: string, + ): Promise { + return this.find({ domain, communityId, postId, deleted: false }); + } +} diff --git a/packages/orm-models/src/repositories/community-post.ts b/packages/orm-models/src/repositories/community-post.ts new file mode 100644 index 000000000..aece602e5 --- /dev/null +++ b/packages/orm-models/src/repositories/community-post.ts @@ -0,0 +1,34 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { + CommunityPostSchema, + type InternalCommunityPost, +} from "../models/community-post"; + +export class CommunityPostRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.CommunityPost || + mongoose.model( + "CommunityPost", + CommunityPostSchema, + )) as Model), + ); + } + + async findByPost( + domain: mongoose.Types.ObjectId, + communityId: string, + postId: string, + ): Promise { + return this.findOne({ domain, communityId, postId }); + } + + async findByCommunity( + domain: mongoose.Types.ObjectId, + communityId: string, + ): Promise { + return this.find({ domain, communityId, deleted: false }); + } +} diff --git a/packages/orm-models/src/repositories/community.ts b/packages/orm-models/src/repositories/community.ts new file mode 100644 index 000000000..49632fe5b --- /dev/null +++ b/packages/orm-models/src/repositories/community.ts @@ -0,0 +1,30 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { CommunitySchema, type InternalCommunity } from "../models/community"; + +export class CommunityRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Community || + mongoose.model( + "Community", + CommunitySchema, + )) as Model), + ); + } + + async findBySlug( + domain: mongoose.Types.ObjectId, + slug: string, + ): Promise { + return this.findOne({ domain, slug }); + } + + async findByCommunityId( + domain: mongoose.Types.ObjectId, + communityId: string, + ): Promise { + return this.findOne({ domain, communityId }); + } +} diff --git a/packages/orm-models/src/repositories/course.ts b/packages/orm-models/src/repositories/course.ts new file mode 100644 index 000000000..9c66af37e --- /dev/null +++ b/packages/orm-models/src/repositories/course.ts @@ -0,0 +1,36 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { CourseSchema, type InternalCourse } from "../models/course"; + +export class CourseRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Course || + mongoose.model( + "Course", + CourseSchema, + )) as Model), + ); + } + + async findBySlug( + domain: mongoose.Types.ObjectId, + slug: string, + ): Promise { + return this.findOne({ domain, slug }); + } + + async findByCourseId( + domain: mongoose.Types.ObjectId, + courseId: string, + ): Promise { + return this.findOne({ domain, courseId }); + } + + async findPublishedByDomain( + domain: mongoose.Types.ObjectId, + ): Promise { + return this.find({ domain, published: true }); + } +} diff --git a/packages/orm-models/src/repositories/domain.ts b/packages/orm-models/src/repositories/domain.ts new file mode 100644 index 000000000..801233c08 --- /dev/null +++ b/packages/orm-models/src/repositories/domain.ts @@ -0,0 +1,21 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { DomainSchema, type Domain } from "../models/domain"; + +export class DomainRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Domain || + mongoose.model("Domain", DomainSchema)) as Model), + ); + } + + async findByName(name: string): Promise { + return this.findOne({ name }); + } + + async findByCustomDomain(customDomain: string): Promise { + return this.findOne({ customDomain }); + } +} diff --git a/packages/orm-models/src/repositories/lesson.ts b/packages/orm-models/src/repositories/lesson.ts new file mode 100644 index 000000000..e95946633 --- /dev/null +++ b/packages/orm-models/src/repositories/lesson.ts @@ -0,0 +1,30 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { LessonSchema, type InternalLesson } from "../models/lesson"; + +export class LessonRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Lesson || + mongoose.model( + "Lesson", + LessonSchema, + )) as Model), + ); + } + + async findByLessonIdAndDomain( + domain: mongoose.Types.ObjectId, + lessonId: string, + ): Promise { + return this.findOne({ domain, lessonId }); + } + + async findByCourseId( + domain: mongoose.Types.ObjectId, + courseId: string, + ): Promise { + return this.find({ domain, courseId }); + } +} diff --git a/packages/orm-models/src/repositories/media.ts b/packages/orm-models/src/repositories/media.ts new file mode 100644 index 000000000..98f003b22 --- /dev/null +++ b/packages/orm-models/src/repositories/media.ts @@ -0,0 +1,22 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { MediaSchema, type MediaWithOwner } from "../models/media"; + +type MediaDocument = MediaWithOwner & mongoose.Document; + +export class MediaRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Media || + mongoose.model( + "Media", + MediaSchema, + )) as Model), + ); + } + + async findByMediaId(mediaId: string): Promise { + return this.findOne({ mediaId }); + } +} diff --git a/packages/orm-models/src/repositories/membership.ts b/packages/orm-models/src/repositories/membership.ts new file mode 100644 index 000000000..6a4077963 --- /dev/null +++ b/packages/orm-models/src/repositories/membership.ts @@ -0,0 +1,34 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { + MembershipSchema, + type InternalMembership, +} from "../models/membership"; + +export class MembershipRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Membership || + mongoose.model( + "Membership", + MembershipSchema, + )) as Model), + ); + } + + async findByUserAndEntity( + domain: mongoose.Types.ObjectId, + userId: string, + entityId: string, + ): Promise { + return this.find({ domain, userId, entityId }); + } + + async findByUser( + domain: mongoose.Types.ObjectId, + userId: string, + ): Promise { + return this.find({ domain, userId }); + } +} diff --git a/packages/orm-models/src/repositories/notification.ts b/packages/orm-models/src/repositories/notification.ts new file mode 100644 index 000000000..77ea46f37 --- /dev/null +++ b/packages/orm-models/src/repositories/notification.ts @@ -0,0 +1,26 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { + NotificationSchema, + type InternalNotification, +} from "../models/notification"; + +export class NotificationRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Notification || + mongoose.model( + "Notification", + NotificationSchema, + )) as Model), + ); + } + + async findByUser( + domain: mongoose.Types.ObjectId, + userId: string, + ): Promise { + return this.find({ domain, userId }); + } +} diff --git a/packages/orm-models/src/repositories/page.ts b/packages/orm-models/src/repositories/page.ts new file mode 100644 index 000000000..06a3cf33b --- /dev/null +++ b/packages/orm-models/src/repositories/page.ts @@ -0,0 +1,20 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { PageSchema, type InternalPage } from "../models/page"; + +export class PageRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.Page || + mongoose.model("Page", PageSchema)) as Model), + ); + } + + async findByPageIdAndDomain( + domain: mongoose.Types.ObjectId, + pageId: string, + ): Promise { + return this.findOne({ domain, pageId }); + } +} diff --git a/packages/orm-models/src/repositories/payment-plan.ts b/packages/orm-models/src/repositories/payment-plan.ts new file mode 100644 index 000000000..3c5189847 --- /dev/null +++ b/packages/orm-models/src/repositories/payment-plan.ts @@ -0,0 +1,33 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { + PaymentPlanSchema, + type InternalPaymentPlan, +} from "../models/payment-plan"; + +export class PaymentPlanRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.PaymentPlan || + mongoose.model( + "PaymentPlan", + PaymentPlanSchema, + )) as Model), + ); + } + + async findByPlanId( + domain: mongoose.Types.ObjectId, + planId: string, + ): Promise { + return this.findOne({ domain, planId }); + } + + async findByEntity( + domain: mongoose.Types.ObjectId, + entityId: string, + ): Promise { + return this.find({ domain, entityId, archived: false }); + } +} diff --git a/packages/orm-models/src/repositories/site-info.ts b/packages/orm-models/src/repositories/site-info.ts new file mode 100644 index 000000000..c1ca734ef --- /dev/null +++ b/packages/orm-models/src/repositories/site-info.ts @@ -0,0 +1,19 @@ +import mongoose, { type Model } from "mongoose"; +import type { SiteInfo } from "@courselit/common-models"; +import { BaseRepository } from "./base"; +import { SettingsSchema } from "../models/site-info"; + +type SiteInfoDocument = SiteInfo & mongoose.Document; + +export class SiteInfoRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.SiteInfo || + mongoose.model( + "SiteInfo", + SettingsSchema, + )) as Model), + ); + } +} diff --git a/packages/orm-models/src/repositories/user.ts b/packages/orm-models/src/repositories/user.ts new file mode 100644 index 000000000..9048eea6d --- /dev/null +++ b/packages/orm-models/src/repositories/user.ts @@ -0,0 +1,27 @@ +import mongoose, { type Model } from "mongoose"; +import { BaseRepository } from "./base"; +import { UserSchema, type InternalUser } from "../models/user"; + +export class UserRepository extends BaseRepository { + constructor(model?: Model) { + super( + model ?? + ((mongoose.models.User || + mongoose.model("User", UserSchema)) as Model), + ); + } + + async findByEmail( + domain: mongoose.Types.ObjectId, + email: string, + ): Promise { + return this.findOne({ domain, email }); + } + + async findByUserIdAndDomain( + domain: mongoose.Types.ObjectId, + userId: string, + ): Promise { + return this.findOne({ domain, userId }); + } +} From 2721492d1120c67041da76f7838225133814bb04 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 11 Jul 2026 06:51:56 +0000 Subject: [PATCH 2/3] feat: migrate GraphQL resolvers to repository pattern (#822) --- apps/web/graphql/courses/helpers.ts | 15 ++++++++--- apps/web/graphql/courses/logic.ts | 27 ++++++++++++------- apps/web/graphql/users/helpers.ts | 15 ++++++----- apps/web/graphql/users/logic.ts | 42 +++++++++++++++++------------ 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/apps/web/graphql/courses/helpers.ts b/apps/web/graphql/courses/helpers.ts index 9bccf45ba..3c1486ef1 100644 --- a/apps/web/graphql/courses/helpers.ts +++ b/apps/web/graphql/courses/helpers.ts @@ -7,9 +7,16 @@ import { slugify } from "@courselit/utils"; import { addGroup } from "./logic"; import { Constants, Course, Progress, User } from "@courselit/common-models"; import { getPlans } from "../paymentplans/logic"; -import { InternalCourse } from "@courselit/orm-models"; +import { + CourseRepository, + InternalCourse, + PageRepository, +} from "@courselit/orm-models"; import { generateUniquePageId, isDuplicateKeyError } from "../pages/helpers"; +const courseRepo = new CourseRepository(CourseModel); +const pageRepo = new PageRepository(Page); + export const validateCourse = async ( courseData: InternalCourse, ctx: GQLContext, @@ -130,14 +137,14 @@ export const setupCourse = async ({ let page; let course; try { - page = await Page.create({ + page = await pageRepo.create({ domain: ctx.subdomain._id, name: title, creatorId: ctx.user.userId, pageId, }); - course = await CourseModel.create({ + course = await courseRepo.create({ domain: ctx.subdomain._id, title: title, cost: 0, @@ -176,7 +183,7 @@ export const setupBlog = async ({ ctx: GQLContext; }) => { try { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: ctx.subdomain._id, title: title, cost: 0, diff --git a/apps/web/graphql/courses/logic.ts b/apps/web/graphql/courses/logic.ts index b653f1852..58dc4e379 100644 --- a/apps/web/graphql/courses/logic.ts +++ b/apps/web/graphql/courses/logic.ts @@ -5,6 +5,10 @@ import CourseModel from "@/models/Course"; import { deleteProductDiscussionData, InternalCourse, + CourseRepository, + LessonRepository, + MembershipRepository, + UserRepository, } from "@courselit/orm-models"; import UserModel from "@/models/User"; import { Media, User } from "@courselit/common-models"; @@ -62,6 +66,11 @@ import { const { open, itemsPerPage, blogPostSnippetLength, permissions } = constants; +const courseRepo = new CourseRepository(CourseModel); +const lessonRepo = new LessonRepository(LessonModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const userRepo = new UserRepository(UserModel); + export const getCourseOrThrow = async ( id: mongoose.Types.ObjectId | undefined, ctx: GQLContext, @@ -77,7 +86,7 @@ export const getCourseOrThrow = async ( _id: id, }; - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ ...query, domain: ctx.subdomain._id, }); @@ -468,7 +477,7 @@ export const getCoursesAsAdmin = async ({ return courses.map(async (course) => ({ ...course, - customers: await (MembershipModel as any).countDocuments({ + customers: await membershipRepo.count({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, domain: context.subdomain._id, @@ -676,7 +685,7 @@ export const getProducts = async ({ for (const course of courses) { const customers = hasManagePerm && course.type !== constants.blog - ? await (MembershipModel as any).countDocuments({ + ? await membershipRepo.count({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, domain: ctx.subdomain._id, @@ -736,7 +745,7 @@ export const getProductsCount = async ({ }) => { const query = getProductsQuery(ctx, filterBy, tags, ids, publicView); - return await (CourseModel as any).countDocuments(query); + return await courseRepo.count(query); }; export const addGroup = async ({ @@ -800,7 +809,7 @@ export const removeGroup = async ( throw new Error(responses.download_course_last_group_cannot_be_removed); } - const countOfAssociatedLessons = await LessonModel.countDocuments({ + const countOfAssociatedLessons = await lessonRepo.count({ courseId, groupId: group.id, domain: ctx.subdomain._id, @@ -974,10 +983,10 @@ export const moveLesson = async ({ ctx: GQLContext; }) => { const course = await getCourseOrThrow(undefined, ctx, courseId); - const lesson = await LessonModel.findOne({ - domain: ctx.subdomain._id, + const lesson = await lessonRepo.findByLessonIdAndDomain( + ctx.subdomain._id, lessonId, - }); + ); if (!lesson || lesson.courseId !== course.courseId) { throw new Error(responses.item_not_found); @@ -1172,7 +1181,7 @@ export const getMembers = async ({ return await Promise.all( members.map(async (member) => { - const user = await UserModel.findOne({ + const user = await userRepo.findOne({ domain: ctx.subdomain._id, userId: member.userId, }); diff --git a/apps/web/graphql/users/helpers.ts b/apps/web/graphql/users/helpers.ts index bbe064c48..d693b0671 100644 --- a/apps/web/graphql/users/helpers.ts +++ b/apps/web/graphql/users/helpers.ts @@ -6,8 +6,9 @@ import constants from "@/config/constants"; import GQLContext from "@/models/GQLContext"; import { InternalUser, - InternalMembership, InternalCourse, + MembershipRepository, + UserRepository, } from "@courselit/orm-models"; import { Constants, UIConstants } from "@courselit/common-models"; import CourseModel from "@models/Course"; @@ -47,6 +48,9 @@ import Account from "@models/Account"; const { permissions } = UIConstants; +const userRepo = new UserRepository(UserModel); +const membershipRepo = new MembershipRepository(MembershipModel); + const CRITICAL_PERMISSIONS = [ permissions.manageSite, permissions.manageSettings, @@ -67,7 +71,7 @@ export async function validateUserDeletion( ): Promise { for (const permission of CRITICAL_PERMISSIONS) { if (userToDelete.permissions?.includes(permission)) { - const otherUsersWithPermission = await UserModel.countDocuments({ + const otherUsersWithPermission = await userRepo.count({ domain: ctx.subdomain._id, userId: { $ne: userToDelete.userId }, permissions: permission, @@ -199,7 +203,7 @@ export async function migrateBusinessEntities( // ========================================== // COMMUNITY MODERATOR ROLE MIGRATION // ========================================== - const creatorMemberships = await MembershipModel.find({ + const creatorMemberships = await membershipRepo.find({ domain: ctx.subdomain._id, userId: userToDelete.userId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -212,8 +216,7 @@ export async function migrateBusinessEntities( for (const membership of creatorMemberships) { communityIds.add(membership.entityId); - const existingMembership = - await MembershipModel.findOne({ + const existingMembership = await membershipRepo.findOne({ domain: ctx.subdomain._id, userId: deleterUser.userId, entityId: membership.entityId, @@ -371,7 +374,7 @@ export async function cleanupPersonalData( ); // Delete memberships and cancel subscriptions - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: ctx.subdomain._id, userId: userToDelete.userId, }); diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts index 7b61c3d73..8346fd512 100644 --- a/apps/web/graphql/users/logic.ts +++ b/apps/web/graphql/users/logic.ts @@ -10,8 +10,12 @@ import { Domain } from "@models/Domain"; import { checkPermission, getEmailFrom, getPlanPrice } from "@courselit/utils"; import UserSegmentModel from "@models/UserSegment"; import { + CourseRepository, + CommunityRepository, InternalCourse, InternalUser, + MembershipRepository, + UserRepository, UserSegment, } from "@courselit/orm-models"; import { Course, UIConstants, User } from "@courselit/common-models"; @@ -46,6 +50,11 @@ import { } from "../paymentplans/logic"; import { convertFiltersToDBConditions } from "@courselit/common-logic"; import { InternalMembership } from "@courselit/orm-models"; + +const userRepo = new UserRepository(UserModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const courseRepo = new CourseRepository(CourseModel); +const communityRepo = new CommunityRepository(CommunityModel); import CertificateModel from "@models/Certificate"; import CertificateTemplateModel, { CertificateTemplate, @@ -76,7 +85,7 @@ export const getUser = async ( let user: any = ctx.user; if (userId) { - user = await UserModel.findOne({ userId, domain: ctx.subdomain._id }); + user = await userRepo.findOne({ userId, domain: ctx.subdomain._id }); } if (!user && !userId) { @@ -131,7 +140,7 @@ export const updateUser = async (userData: UserData, ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - let user = await UserModel.findOne({ + let user = await userRepo.findOne({ userId: id, domain: ctx.subdomain._id, }); @@ -197,7 +206,7 @@ export const inviteCustomer = async ( } const sanitizedEmail = sanitizeEmail(email); - let user = await UserModel.findOne({ + let user = await userRepo.findOne({ email: sanitizedEmail, domain: ctx.subdomain._id, }); @@ -266,7 +275,7 @@ export const deleteUser = async ( throw new Error(responses.action_not_allowed); } - const userToDelete = await UserModel.findOne({ + const userToDelete = await userRepo.findOne({ domain: ctx.subdomain._id, userId, }); @@ -283,7 +292,7 @@ export const deleteUser = async ( } const deleterUser = - (await UserModel.findOne({ + (await userRepo.findOne({ domain: ctx.subdomain._id, userId: ctx.user.userId, })) || (ctx.user as InternalUser); @@ -343,7 +352,7 @@ export const getUsersCount = async (ctx: GQLContext, filters?: string) => { } const query = await buildQueryFromSearchData(ctx.subdomain._id, filters); - return await UserModel.countDocuments(query); + return await userRepo.count(query); }; const buildQueryFromSearchData = async ( @@ -702,7 +711,7 @@ export const getUserContent = async ( id = userId; } - const user = await UserModel.findOne({ + const user = await userRepo.findOne({ userId: id, domain: ctx.subdomain._id, }); @@ -715,7 +724,7 @@ export const getUserContent = async ( }; async function getUserContentInternal(ctx: GQLContext, user: User) { - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: ctx.subdomain._id, userId: user.userId, status: Constants.MembershipStatus.ACTIVE, @@ -735,7 +744,7 @@ async function getUserContentInternal(ctx: GQLContext, user: User) { continue; } - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ courseId: membership.entityId, domain: ctx.subdomain._id, }); @@ -783,7 +792,7 @@ async function getUserContentInternal(ctx: GQLContext, user: User) { if ( membership.entityType === Constants.MembershipEntityType.COMMUNITY ) { - const community = await CommunityModel.findOne({ + const community = await communityRepo.findOne({ communityId: membership.entityId, domain: ctx.subdomain._id, deleted: false, @@ -816,7 +825,7 @@ export const getMembershipStatus = async ({ }): Promise => { checkIfAuthenticated(ctx); - const membership: Membership | null = await MembershipModel.findOne({ + const membership: Membership | null = await membershipRepo.findOne({ domain: ctx.subdomain._id, entityId, entityType, @@ -858,8 +867,7 @@ export const getMembership = async ({ entityId: string; planId: string; }): Promise => { - const existingMembership = - await MembershipModel.findOne({ + const existingMembership = await membershipRepo.findOne({ domain: domainId, userId, entityType, @@ -868,7 +876,7 @@ export const getMembership = async ({ let membership: InternalMembership = existingMembership || - (await MembershipModel.create({ + (await membershipRepo.create({ domain: domainId, userId, paymentPlanId: planId, @@ -891,7 +899,7 @@ export async function findMembership({ entityId: string; entityType?: MembershipEntityType; }): Promise { - return MembershipModel.findOne({ + return membershipRepo.findOne({ domain: domainId, userId, entityType, @@ -908,7 +916,7 @@ export async function runPostMembershipTasks({ membership: Membership; paymentPlan: PaymentPlan; }) { - const user = await UserModel.findOne({ + const user = await userRepo.findOne({ userId: membership.userId, }); if (!user) { @@ -942,7 +950,7 @@ export async function runPostMembershipTasks({ event = Constants.EventType.COMMUNITY_JOINED as unknown as Event; } if (membership.entityType === Constants.MembershipEntityType.COURSE) { - const product = await CourseModel.findOne({ + const product = await courseRepo.findOne({ courseId: membership.entityId, }); if (product) { From a235ed547e97ea20bc38a85b7eeb0deac28c1771 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 11 Jul 2026 07:34:15 +0000 Subject: [PATCH 3/3] feat: migrate all GraphQL resolvers to repository pattern (#822) --- .../__tests__/create-community-post.test.ts | 43 ++-- .../__tests__/delete-community-post.test.ts | 57 ++++-- .../__tests__/delete-community.test.ts | 145 +++++++------ .../communities/__tests__/logic.test.ts | 81 +++++--- .../communities/__tests__/slug.test.ts | 25 ++- .../__tests__/update-community-post.test.ts | 47 +++-- apps/web/graphql/communities/helpers.ts | 17 +- apps/web/graphql/communities/logic.ts | 171 ++++++++-------- .../courses/__tests__/delete-course.test.ts | 193 ++++++++++-------- .../graphql/courses/__tests__/logic.test.ts | 117 ++++++----- .../courses/__tests__/move-lesson.test.ts | 43 ++-- .../courses/__tests__/reorder-groups.test.ts | 33 +-- .../graphql/courses/__tests__/slug.test.ts | 21 +- .../__tests__/update-group-drip.test.ts | 25 ++- .../__tests__/update-group-metadata.test.ts | 15 +- .../graphql/lessons/__tests__/scorm.test.ts | 31 ++- .../lessons/__tests__/visibility.test.ts | 39 ++-- apps/web/graphql/lessons/helpers.ts | 8 +- apps/web/graphql/lessons/logic.ts | 25 ++- .../web/graphql/mails/__tests__/logic.test.ts | 5 +- apps/web/graphql/mails/logic.ts | 14 +- apps/web/graphql/menus/logic.ts | 8 +- .../notifications/__tests__/logic.test.ts | 39 ++-- apps/web/graphql/notifications/logic.ts | 8 +- .../web/graphql/pages/__tests__/logic.test.ts | 119 ++++++----- apps/web/graphql/pages/__tests__/slug.test.ts | 20 +- apps/web/graphql/pages/helpers.ts | 5 +- apps/web/graphql/pages/logic.ts | 22 +- apps/web/graphql/paymentplans/logic.ts | 36 ++-- .../graphql/product-discussions/helpers.ts | 8 +- apps/web/graphql/product-discussions/logic.ts | 7 +- apps/web/graphql/product-discussions/types.ts | 15 +- .../settings/__tests__/payment.test.ts | 12 +- .../graphql/settings/__tests__/sso.test.ts | 18 +- apps/web/graphql/settings/logic.ts | 13 +- .../users/__tests__/delete-user.test.ts | 129 +++++++----- .../web/graphql/users/__tests__/logic.test.ts | 77 ++++--- apps/web/graphql/users/helpers.ts | 10 +- apps/web/graphql/users/logic.ts | 10 +- 39 files changed, 1041 insertions(+), 670 deletions(-) diff --git a/apps/web/graphql/communities/__tests__/create-community-post.test.ts b/apps/web/graphql/communities/__tests__/create-community-post.test.ts index acb54aade..275e5a3cc 100644 --- a/apps/web/graphql/communities/__tests__/create-community-post.test.ts +++ b/apps/web/graphql/communities/__tests__/create-community-post.test.ts @@ -13,6 +13,23 @@ import UserModel from "@models/User"; import CommunityPostSubscriberModel from "@models/CommunityPostSubscriber"; import constants from "@/config/constants"; import { Constants, TextEditorContent } from "@courselit/common-models"; +import { + CommunityPostRepository, + UserRepository, + MembershipRepository, + PaymentPlanRepository, + PageRepository, + CommunityRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit"); jest.mock("@/services/queue"); @@ -47,12 +64,12 @@ describe("createCommunityPost", () => { let commentOnlyCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin"), email: email("admin"), @@ -62,7 +79,7 @@ describe("createCommunityPost", () => { unsubscribeToken: id("unsub-admin"), }); - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: id("regular"), email: email("regular"), @@ -72,7 +89,7 @@ describe("createCommunityPost", () => { unsubscribeToken: id("unsub-regular"), }); - commentOnlyUser = await UserModel.create({ + commentOnlyUser = await userRepo.create({ domain: testDomain._id, userId: id("comment-only"), email: email("comment-only"), @@ -83,7 +100,7 @@ describe("createCommunityPost", () => { }); // Internal payment plan (required by the system) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("internal-plan"), userId: adminUser.userId, @@ -97,7 +114,7 @@ describe("createCommunityPost", () => { currencyISOCode: "USD", }); - community = await CommunityModel.create({ + community = await communityRepo.create({ domain: testDomain._id, communityId: id("community"), name: "Test Community", @@ -108,7 +125,7 @@ describe("createCommunityPost", () => { categories: ["General", "Announcements"], }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -118,7 +135,7 @@ describe("createCommunityPost", () => { }); // Free payment plan for the community - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("free-plan"), userId: adminUser.userId, @@ -132,7 +149,7 @@ describe("createCommunityPost", () => { }); // Admin membership (MODERATE role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("admin-membership"), userId: adminUser.userId, @@ -145,7 +162,7 @@ describe("createCommunityPost", () => { }); // Regular user membership (POST role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("regular-membership"), userId: regularUser.userId, @@ -158,7 +175,7 @@ describe("createCommunityPost", () => { }); // Comment-only user membership (COMMENT role — cannot post) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("comment-membership"), userId: commentOnlyUser.userId, @@ -229,7 +246,7 @@ describe("createCommunityPost", () => { }); it("should throw action_not_allowed for a user with no membership", async () => { - const noMemberUser = await UserModel.create({ + const noMemberUser = await userRepo.create({ domain: testDomain._id, userId: id("no-member"), email: email("no-member"), @@ -313,7 +330,7 @@ describe("createCommunityPost", () => { ctx: regularCtx, }); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ domain: testDomain._id, postId: result.postId, }); diff --git a/apps/web/graphql/communities/__tests__/delete-community-post.test.ts b/apps/web/graphql/communities/__tests__/delete-community-post.test.ts index 5b95651b6..771485e77 100644 --- a/apps/web/graphql/communities/__tests__/delete-community-post.test.ts +++ b/apps/web/graphql/communities/__tests__/delete-community-post.test.ts @@ -14,6 +14,27 @@ import UserModel from "@models/User"; import CommunityPostSubscriberModel from "@models/CommunityPostSubscriber"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; +import { + CommunityPostRepository, + CommunityCommentRepository, + UserRepository, + MembershipRepository, + PaymentPlanRepository, + PageRepository, + CommunityRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit"); jest.mock("@/services/queue"); @@ -40,12 +61,12 @@ describe("deleteCommunityPost", () => { let existingPost: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin"), email: email("admin"), @@ -55,7 +76,7 @@ describe("deleteCommunityPost", () => { unsubscribeToken: id("unsub-admin"), }); - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: id("regular"), email: email("regular"), @@ -65,7 +86,7 @@ describe("deleteCommunityPost", () => { unsubscribeToken: id("unsub-regular"), }); - otherMemberUser = await UserModel.create({ + otherMemberUser = await userRepo.create({ domain: testDomain._id, userId: id("other"), email: email("other"), @@ -76,7 +97,7 @@ describe("deleteCommunityPost", () => { }); // Internal payment plan (required by the system) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("internal-plan"), userId: adminUser.userId, @@ -90,7 +111,7 @@ describe("deleteCommunityPost", () => { currencyISOCode: "USD", }); - community = await CommunityModel.create({ + community = await communityRepo.create({ domain: testDomain._id, communityId: id("community"), name: "Test Community", @@ -101,7 +122,7 @@ describe("deleteCommunityPost", () => { categories: ["General", "Announcements"], }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -111,7 +132,7 @@ describe("deleteCommunityPost", () => { }); // Free payment plan for the community - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("free-plan"), userId: adminUser.userId, @@ -125,7 +146,7 @@ describe("deleteCommunityPost", () => { }); // Admin membership (MODERATE role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("admin-membership"), userId: adminUser.userId, @@ -138,7 +159,7 @@ describe("deleteCommunityPost", () => { }); // Regular user membership (POST role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("regular-membership"), userId: regularUser.userId, @@ -151,7 +172,7 @@ describe("deleteCommunityPost", () => { }); // Other member membership (POST role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("other-membership"), userId: otherMemberUser.userId, @@ -218,7 +239,7 @@ describe("deleteCommunityPost", () => { }); it("should throw action_not_allowed if missing membership", async () => { - const noMemberUser = await UserModel.create({ + const noMemberUser = await userRepo.create({ domain: testDomain._id, userId: id("no-member-del"), email: email("no-member-del"), @@ -263,7 +284,7 @@ describe("deleteCommunityPost", () => { ctx: regularCtx, }); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost).toBeNull(); @@ -276,14 +297,14 @@ describe("deleteCommunityPost", () => { ctx: adminCtx, }); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost).toBeNull(); }); it("should delete associated comments", async () => { - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: existingPost.postId, @@ -298,7 +319,7 @@ describe("deleteCommunityPost", () => { ctx: regularCtx, }); - const dbComments = await CommunityCommentModel.find({ + const dbComments = await communityCommentRepo.find({ postId: existingPost.postId, }); expect(dbComments.length).toBe(0); @@ -349,7 +370,7 @@ describe("deleteCommunityPost", () => { ); // Post should be deleted regardless of deleteMedia failure - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost).toBeNull(); @@ -391,7 +412,7 @@ describe("deleteCommunityPost", () => { expect(medialit.deleteMedia).toHaveBeenCalledWith("media-success"); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost).toBeNull(); diff --git a/apps/web/graphql/communities/__tests__/delete-community.test.ts b/apps/web/graphql/communities/__tests__/delete-community.test.ts index d819d2fbc..350f60bb7 100644 --- a/apps/web/graphql/communities/__tests__/delete-community.test.ts +++ b/apps/web/graphql/communities/__tests__/delete-community.test.ts @@ -13,6 +13,27 @@ import UserModel from "@models/User"; import InvoiceModel from "@models/Invoice"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; +import { + CommunityRepository, + PageRepository, + PaymentPlanRepository, + MembershipRepository, + CommunityCommentRepository, + CommunityPostRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit"); jest.mock("@/services/queue"); @@ -30,13 +51,13 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { beforeAll(async () => { // Create unique test domain - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: `test-domain-dc-${Date.now()}`, email: "test@example.com", }); // Create admin user - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: "admin-user", email: "admin@example.com", @@ -47,7 +68,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); // Create internal payment plan (required by deleteMemberships) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "internal-plan", userId: adminUser.userId, @@ -102,7 +123,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should require manageCommunity permission", async () => { - const regularUser = await UserModel.create({ + const regularUser = await userRepo.create({ domain: testDomain._id, userId: "regular-user", email: "regular@example.com", @@ -131,7 +152,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should not delete disabled community without permission", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-disabled-comm", name: "Disabled Community", @@ -141,7 +162,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - const regularUser = await UserModel.create({ + const regularUser = await userRepo.create({ domain: testDomain._id, userId: "regular-user-2", email: "regular2@example.com", @@ -166,7 +187,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { describe("Community Content Cleanup", () => { it("should delete all community posts", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-posts", name: "Community with Posts", @@ -176,7 +197,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -185,7 +206,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - await CommunityPostModel.create({ + await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "post-1", @@ -197,7 +218,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const posts = await CommunityPostModel.find({ + const posts = await communityPostRepo.find({ domain: testDomain._id, communityId: community.communityId, }); @@ -205,7 +226,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete all community comments", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-comments", name: "Community with Comments", @@ -215,7 +236,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -224,7 +245,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const post = await CommunityPostModel.create({ + const post = await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "post-2", @@ -234,7 +255,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { category: "general", }); - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -245,7 +266,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const comments = await CommunityCommentModel.find({ + const comments = await communityCommentRepo.find({ domain: testDomain._id, communityId: community.communityId, }); @@ -253,7 +274,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete all community reports", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-reports", name: "Community with Reports", @@ -263,7 +284,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -292,7 +313,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete post subscriptions", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-subs", name: "Community with Subscriptions", @@ -302,7 +323,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -311,7 +332,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const post = await CommunityPostModel.create({ + const post = await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "post-3", @@ -340,7 +361,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { describe("Membership & Payment Plan Cleanup", () => { it("should delete all community memberships", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-members", name: "Community with Members", @@ -350,7 +371,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -359,7 +380,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const plan = await PaymentPlanModel.create({ + const plan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-1", userId: adminUser.userId, @@ -372,7 +393,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { currencyISOCode: "USD", }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "membership-1", userId: adminUser.userId, @@ -385,7 +406,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: testDomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -400,7 +421,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { cancel: mockCancel, }); - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-subscriptions", name: "Community with Subscriptions", @@ -410,7 +431,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -419,7 +440,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const plan = await PaymentPlanModel.create({ + const plan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-subscription", userId: adminUser.userId, @@ -432,7 +453,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { currencyISOCode: "USD", }); - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "membership-subscription", userId: adminUser.userId, @@ -462,7 +483,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { expect(mockCancel).toHaveBeenCalledWith("sub_stripe_456"); // Verify membership was deleted - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: testDomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -478,7 +499,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete all payment plans", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-plans", name: "Community with Plans", @@ -488,7 +509,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -497,7 +518,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-2", userId: adminUser.userId, @@ -512,7 +533,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const plans = await PaymentPlanModel.find({ + const plans = await paymentPlanRepo.find({ domain: testDomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -521,7 +542,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete memberships with included products", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-included", name: "Community with Included Products", @@ -531,7 +552,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -540,7 +561,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const plan = await PaymentPlanModel.create({ + const plan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-included", userId: adminUser.userId, @@ -554,7 +575,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { includedProducts: ["course-1", "course-2"], }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "membership-included", userId: adminUser.userId, @@ -578,7 +599,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const courseMemberships = await MembershipModel.find({ + const courseMemberships = await membershipRepo.find({ domain: testDomain._id, paymentPlanId: plan.planId, isIncludedInPlan: true, @@ -596,7 +617,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { describe("Page & Media Cleanup", () => { it("should delete community page", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-page", name: "Community with Page", @@ -606,7 +627,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -617,7 +638,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const page = await PageModel.findOne({ + const page = await pageRepo.findOne({ domain: testDomain._id, pageId: community.pageId, }); @@ -625,7 +646,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should delete community media", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-media", name: "Community with Media", @@ -645,7 +666,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -663,7 +684,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { describe("Community Document Cleanup", () => { it("should delete the community document", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-delete", name: "Community to Delete", @@ -673,7 +694,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -684,7 +705,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const deletedCommunity = await CommunityModel.findOne({ + const deletedCommunity = await communityRepo.findOne({ domain: testDomain._id, communityId: community.communityId, }); @@ -694,7 +715,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { describe("Integration Tests", () => { it("should handle complex scenario with all entities", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-complex", name: "Complex Community", @@ -714,7 +735,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -723,7 +744,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { entityId: community.communityId, }); - const plan = await PaymentPlanModel.create({ + const plan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-complex", userId: adminUser.userId, @@ -736,7 +757,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { currencyISOCode: "USD", }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "membership-complex", userId: adminUser.userId, @@ -747,7 +768,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { sessionId: "session-complex", }); - const post = await CommunityPostModel.create({ + const post = await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "post-complex", @@ -757,7 +778,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { category: "general", }); - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -796,15 +817,15 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { remainingPlans, remainingPage, ] = await Promise.all([ - CommunityModel.findOne({ + communityRepo.findOne({ domain: testDomain._id, communityId: community.communityId, }), - CommunityPostModel.find({ + communityPostRepo.find({ domain: testDomain._id, communityId: community.communityId, }), - CommunityCommentModel.find({ + communityCommentRepo.find({ domain: testDomain._id, communityId: community.communityId, }), @@ -816,17 +837,17 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { domain: testDomain._id, postId: post.postId, }), - MembershipModel.find({ + membershipRepo.find({ domain: testDomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, }), - PaymentPlanModel.find({ + paymentPlanRepo.find({ domain: testDomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, }), - PageModel.findOne({ + pageRepo.findOne({ domain: testDomain._id, pageId: community.pageId, }), @@ -844,7 +865,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { }); it("should successfully delete empty community", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "dc-comm-empty", name: "Empty Community", @@ -854,7 +875,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { deleted: false, }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -865,7 +886,7 @@ describe("deleteCommunity - Comprehensive Test Suite", () => { await deleteCommunity({ ctx: mockCtx, id: community.communityId }); - const deletedCommunity = await CommunityModel.findOne({ + const deletedCommunity = await communityRepo.findOne({ domain: testDomain._id, communityId: community.communityId, }); diff --git a/apps/web/graphql/communities/__tests__/logic.test.ts b/apps/web/graphql/communities/__tests__/logic.test.ts index 0ffd9dcae..95f73f309 100644 --- a/apps/web/graphql/communities/__tests__/logic.test.ts +++ b/apps/web/graphql/communities/__tests__/logic.test.ts @@ -22,6 +22,27 @@ import DomainModel from "@models/Domain"; import UserModel from "@models/User"; import constants from "@/config/constants"; import { Constants, TextEditorContent } from "@courselit/common-models"; +import { + UserRepository, + DomainRepository, + CommunityPostRepository, + MembershipRepository, + CommunityRepository, + CommunityCommentRepository, + PaymentPlanRepository, + PageRepository, +} from "@courselit/orm-models"; + +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/queue"); @@ -51,13 +72,13 @@ describe("Community Logic - Comment Count Tests", () => { beforeAll(async () => { // Create unique test domain - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: `test-domain-logic-${Date.now()}`, email: "test@example.com", }); // Create admin user - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: "admin-user-logic", email: "admin@example.com", @@ -68,7 +89,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create regular user - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: "regular-user-logic", email: "regular@example.com", @@ -79,7 +100,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create internal payment plan (required for membership) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "internal-plan-logic", userId: adminUser.userId, @@ -99,7 +120,7 @@ describe("Community Logic - Comment Count Tests", () => { } as any; // Create community manually - community = await CommunityModel.create({ + community = await communityRepo.create({ domain: testDomain._id, communityId: "test-comm-logic", name: "Test Community Logic", @@ -111,7 +132,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create page for community - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -121,7 +142,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create free payment plan for the community - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "free-plan-logic", userId: adminUser.userId, @@ -135,7 +156,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create memberships manually for both users - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "admin-membership-logic", userId: adminUser.userId, @@ -147,7 +168,7 @@ describe("Community Logic - Comment Count Tests", () => { role: Constants.MembershipRole.MODERATE, }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "regular-membership-logic", userId: regularUser.userId, @@ -160,7 +181,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create a test post - post = await CommunityPostModel.create({ + post = await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "test-post-logic", @@ -206,7 +227,7 @@ describe("Community Logic - Comment Count Tests", () => { it("should count top-level comments correctly", async () => { // Add 3 top-level comments - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -215,7 +236,7 @@ describe("Community Logic - Comment Count Tests", () => { content: "First comment", }); - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -224,7 +245,7 @@ describe("Community Logic - Comment Count Tests", () => { content: "Second comment", }); - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -239,7 +260,7 @@ describe("Community Logic - Comment Count Tests", () => { it("should include replies in the comment count", async () => { // Create a comment with replies - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -271,7 +292,7 @@ describe("Community Logic - Comment Count Tests", () => { it("should count multiple comments with multiple replies", async () => { // First comment with 2 replies - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -297,7 +318,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Second comment with 1 reply - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -316,7 +337,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Third comment with no replies - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -332,7 +353,7 @@ describe("Community Logic - Comment Count Tests", () => { it("should exclude deleted comments from count", async () => { // Create a deleted comment - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -343,7 +364,7 @@ describe("Community Logic - Comment Count Tests", () => { }); // Create a non-deleted comment - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -360,7 +381,7 @@ describe("Community Logic - Comment Count Tests", () => { it("should count non-deleted replies even when parent comment is deleted", async () => { // Create a deleted comment with non-deleted replies - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: post.postId, @@ -387,7 +408,7 @@ describe("Community Logic - Comment Count Tests", () => { describe("getCommunityReports", () => { it("returns a string preview for reported rich-text posts", async () => { - const richTextPost = await CommunityPostModel.create({ + const richTextPost = await communityPostRepo.create({ domain: testDomain._id, communityId: community.communityId, postId: "reported-rich-text-post", @@ -432,12 +453,12 @@ describe("Community Logic - Feed Tests", () => { let mockCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: `test-domain-feed-${Date.now()}`, email: "feed@example.com", }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: "admin-user-feed", email: "admin-feed@example.com", @@ -447,7 +468,7 @@ describe("Community Logic - Feed Tests", () => { unsubscribeToken: "unsubscribe-admin-feed", }); - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: "regular-user-feed", email: "regular-feed@example.com", @@ -457,7 +478,7 @@ describe("Community Logic - Feed Tests", () => { unsubscribeToken: "unsubscribe-regular-feed", }); - communityOne = await CommunityModel.create({ + communityOne = await communityRepo.create({ domain: testDomain._id, communityId: "community-feed-1", name: "Community Feed One", @@ -467,7 +488,7 @@ describe("Community Logic - Feed Tests", () => { deleted: false, }); - communityTwo = await CommunityModel.create({ + communityTwo = await communityRepo.create({ domain: testDomain._id, communityId: "community-feed-2", name: "Community Feed Two", @@ -572,7 +593,7 @@ describe("Community Logic - Feed Tests", () => { }); it("does not include posts from inactive memberships", async () => { - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "feed-membership-pending", userId: regularUser.userId, @@ -584,7 +605,7 @@ describe("Community Logic - Feed Tests", () => { role: Constants.MembershipRole.POST, }); - await CommunityPostModel.create({ + await communityPostRepo.create({ domain: testDomain._id, userId: adminUser.userId, communityId: communityOne.communityId, @@ -610,12 +631,12 @@ describe("Community Logic - Enabled Communities Count Tests", () => { let mockCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: `test-domain-enabled-count-${Date.now()}`, email: "enabled-count@example.com", }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: "enabled-count-admin", email: "enabled-count-admin@example.com", diff --git a/apps/web/graphql/communities/__tests__/slug.test.ts b/apps/web/graphql/communities/__tests__/slug.test.ts index d60b3fa02..7e2e9a100 100644 --- a/apps/web/graphql/communities/__tests__/slug.test.ts +++ b/apps/web/graphql/communities/__tests__/slug.test.ts @@ -11,6 +11,19 @@ import DomainModel from "@models/Domain"; import UserModel from "@models/User"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; +import { + PageRepository, + CommunityRepository, + PaymentPlanRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/queue"); jest.mock("nanoid", () => ({ @@ -38,12 +51,12 @@ describe("Community Slug Tests", () => { let mockCtx: any; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: domain._id, userId: id("admin"), email: email("admin"), @@ -54,7 +67,7 @@ describe("Community Slug Tests", () => { }); // Internal payment plan (required by createCommunity) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: domain._id, planId: id("internal-plan"), userId: adminUser.userId, @@ -99,7 +112,7 @@ describe("Community Slug Tests", () => { expect(result.slug).toBe("my-test-community"); - const community = await CommunityModel.findOne({ + const community = await communityRepo.findOne({ communityId: result.communityId, }); expect(community?.slug).toBe(community?.pageId); @@ -140,13 +153,13 @@ describe("Community Slug Tests", () => { expect(updated.slug).toBe("new-custom-slug"); - const community = await CommunityModel.findOne({ + const community = await communityRepo.findOne({ communityId: created.communityId, }); expect(community?.slug).toBe("new-custom-slug"); expect(community?.pageId).toBe("new-custom-slug"); - const page = await PageModel.findOne({ + const page = await pageRepo.findOne({ entityId: created.communityId, domain: domain._id, }); diff --git a/apps/web/graphql/communities/__tests__/update-community-post.test.ts b/apps/web/graphql/communities/__tests__/update-community-post.test.ts index d88da5ee3..2b1811261 100644 --- a/apps/web/graphql/communities/__tests__/update-community-post.test.ts +++ b/apps/web/graphql/communities/__tests__/update-community-post.test.ts @@ -13,6 +13,23 @@ import UserModel from "@models/User"; import CommunityPostSubscriberModel from "@models/CommunityPostSubscriber"; import constants from "@/config/constants"; import { Constants, TextEditorContent } from "@courselit/common-models"; +import { + CommunityPostRepository, + UserRepository, + MembershipRepository, + PaymentPlanRepository, + PageRepository, + CommunityRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const domainRepo = new DomainRepository(DomainModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit"); jest.mock("@/services/queue"); @@ -48,12 +65,12 @@ describe("updateCommunityPost", () => { let existingPost: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin"), email: email("admin"), @@ -63,7 +80,7 @@ describe("updateCommunityPost", () => { unsubscribeToken: id("unsub-admin"), }); - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: id("regular"), email: email("regular"), @@ -73,7 +90,7 @@ describe("updateCommunityPost", () => { unsubscribeToken: id("unsub-regular"), }); - otherMemberUser = await UserModel.create({ + otherMemberUser = await userRepo.create({ domain: testDomain._id, userId: id("other"), email: email("other"), @@ -84,7 +101,7 @@ describe("updateCommunityPost", () => { }); // Internal payment plan (required by the system) - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("internal-plan"), userId: adminUser.userId, @@ -98,7 +115,7 @@ describe("updateCommunityPost", () => { currencyISOCode: "USD", }); - community = await CommunityModel.create({ + community = await communityRepo.create({ domain: testDomain._id, communityId: id("community"), name: "Test Community", @@ -109,7 +126,7 @@ describe("updateCommunityPost", () => { categories: ["General", "Announcements"], }); - await PageModel.create({ + await pageRepo.create({ domain: testDomain._id, pageId: community.pageId, type: constants.communityPage, @@ -119,7 +136,7 @@ describe("updateCommunityPost", () => { }); // Free payment plan for the community - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: id("free-plan"), userId: adminUser.userId, @@ -133,7 +150,7 @@ describe("updateCommunityPost", () => { }); // Admin membership (MODERATE role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("admin-membership"), userId: adminUser.userId, @@ -146,7 +163,7 @@ describe("updateCommunityPost", () => { }); // Regular user membership (POST role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("regular-membership"), userId: regularUser.userId, @@ -159,7 +176,7 @@ describe("updateCommunityPost", () => { }); // Other member membership (POST role) - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: id("other-membership"), userId: otherMemberUser.userId, @@ -228,7 +245,7 @@ describe("updateCommunityPost", () => { }); it("should throw action_not_allowed for a user with no membership", async () => { - const noMemberUser = await UserModel.create({ + const noMemberUser = await userRepo.create({ domain: testDomain._id, userId: id("no-member"), email: email("no-member"), @@ -309,7 +326,7 @@ describe("updateCommunityPost", () => { expect(result.content).toEqual(doc("Updated Content")); expect(result.category).toBe("Announcements"); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost!.title).toBe("Updated Title"); @@ -330,7 +347,7 @@ describe("updateCommunityPost", () => { expect(result.category).toBe("General"); // media undefined means do not touch media - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost!.media).toEqual([]); @@ -476,7 +493,7 @@ describe("updateCommunityPost", () => { expect(medialit.deleteMedia).toHaveBeenCalledWith("old-media-1"); - const dbPost = await CommunityPostModel.findOne({ + const dbPost = await communityPostRepo.findOne({ postId: existingPost.postId, }); expect(dbPost!.media.length).toBe(0); diff --git a/apps/web/graphql/communities/helpers.ts b/apps/web/graphql/communities/helpers.ts index 326341260..538b0b3a6 100644 --- a/apps/web/graphql/communities/helpers.ts +++ b/apps/web/graphql/communities/helpers.ts @@ -28,6 +28,15 @@ import { extractTextFromTextEditorContent, normalizeTextEditorContent, } from "@courselit/utils"; +import { + CommunityPostRepository, + CommunityCommentRepository, +} from "@courselit/orm-models"; + +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); export type PublicPost = Omit< CommunityPost, @@ -130,19 +139,19 @@ export async function getCommunityReportContent({ let content: any = undefined; if (type === Constants.CommunityReportType.POST) { - content = await CommunityPostModel.findOne({ + content = await communityPostRepo.findOne({ domain, communityId, postId: contentId, }); } else if (type === Constants.CommunityReportType.COMMENT) { - content = await CommunityCommentModel.findOne({ + content = await communityCommentRepo.findOne({ domain, communityId, commentId: contentId, }); } else if (type === Constants.CommunityReportType.REPLY) { - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ domain, communityId, commentId: contentParentId, @@ -175,7 +184,7 @@ export async function deleteCommunityData( communityId, }); - const posts = await CommunityPostModel.find({ + const posts = await communityPostRepo.find({ domain: ctx.subdomain._id, communityId, }); diff --git a/apps/web/graphql/communities/logic.ts b/apps/web/graphql/communities/logic.ts index 0cab8c071..09cac40a4 100644 --- a/apps/web/graphql/communities/logic.ts +++ b/apps/web/graphql/communities/logic.ts @@ -70,10 +70,27 @@ import getDeletedMediaIds from "@/lib/get-deleted-media-ids"; import { deleteMedia, sealMedia } from "@/services/medialit"; import CommunityPostSubscriberModel from "@models/CommunityPostSubscriber"; import InvoiceModel from "@models/Invoice"; -import { InternalMembership } from "@courselit/orm-models"; +import { + InternalMembership, + CommunityCommentRepository, + CommunityPostRepository, + MembershipRepository, + PaymentPlanRepository, + CommunityRepository, + PageRepository, +} from "@courselit/orm-models"; import { replaceTempMediaWithSealedMediaInProseMirrorDoc } from "@/lib/replace-temp-media-with-sealed-media-in-prosemirror-doc"; import { recordActivity } from "@/lib/record-activity"; +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); + const { permissions, communityPage } = constants; export async function createCommunity({ @@ -89,7 +106,7 @@ export async function createCommunity({ throw new Error(responses.action_not_allowed); } - const existingCommunity = await CommunityModel.findOne({ + const existingCommunity = await communityRepo.findOne({ domain: ctx.subdomain._id, name, deleted: false, @@ -105,7 +122,7 @@ export async function createCommunity({ let community; try { - await PageModel.create({ + await pageRepo.create({ domain: ctx.subdomain._id, pageId, type: communityPage, @@ -130,7 +147,7 @@ export async function createCommunity({ title: name, }); - community = await CommunityModel.create({ + community = await communityRepo.create({ domain: ctx.subdomain._id, communityId, name, @@ -145,7 +162,7 @@ export async function createCommunity({ } const paymentPlan = await getInternalPaymentPlan(ctx); - await MembershipModel.create({ + await membershipRepo.create({ domain: ctx.subdomain._id, userId: ctx.user.userId, entityId: community.communityId, @@ -172,7 +189,7 @@ export async function getCommunity({ deleted: false, }; - const community = await CommunityModel.findOne(query); + const community = await communityRepo.findOne(query); if ( !community || @@ -269,7 +286,7 @@ export async function getCommunitiesCount({ query.enabled = true; } - const count = await (CommunityModel as any).countDocuments(query); + const count = await communityRepo.count(query); return count; } @@ -299,9 +316,7 @@ export async function updateCommunity({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( - getCommunityQuery(ctx, id), - ); + const community = await communityRepo.findOne(getCommunityQuery(ctx, id)); if (!community) { throw new Error(responses.item_not_found); @@ -446,9 +461,7 @@ export async function addCategory({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( - getCommunityQuery(ctx, id), - ); + const community = await communityRepo.findOne(getCommunityQuery(ctx, id)); if (!community) { throw new Error(responses.item_not_found); @@ -486,9 +499,7 @@ export async function deleteCategory({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( - getCommunityQuery(ctx, id), - ); + const community = await communityRepo.findOne(getCommunityQuery(ctx, id)); if (!community) { throw new Error(responses.item_not_found); @@ -532,9 +543,7 @@ export async function joinCommunity({ throw new Error(responses.profile_incomplete); } - const community = await CommunityModel.findOne( - getCommunityQuery(ctx, id), - ); + const community = await communityRepo.findOne(getCommunityQuery(ctx, id)); if (!community) { throw new Error(responses.item_not_found); @@ -550,7 +559,7 @@ export async function joinCommunity({ throw new Error(responses.community_has_no_payment_plans); } - const freePaymentPlanOfCommunity = await PaymentPlanModel.findOne({ + const freePaymentPlanOfCommunity = await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -562,7 +571,7 @@ export async function joinCommunity({ throw new Error(responses.community_requires_payment); } - let member = await MembershipModel.findOne({ + let member = await membershipRepo.findOne({ domain: ctx.subdomain._id, userId: ctx.user.userId, paymentPlanId: freePaymentPlanOfCommunity.planId, @@ -571,7 +580,7 @@ export async function joinCommunity({ }); if (!member) { - member = await MembershipModel.create({ + member = await membershipRepo.create({ domain: ctx.subdomain._id, userId: ctx.user.userId, paymentPlanId: freePaymentPlanOfCommunity.planId, @@ -597,7 +606,7 @@ export async function joinCommunity({ // userId: 1, // }, // ).lean(); - const communityManagers: Membership[] = await MembershipModel.find({ + const communityManagers: Membership[] = await membershipRepo.find({ domain: ctx.subdomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -622,7 +631,7 @@ async function getMembership( ctx: GQLContext, communityId: string, ): Promise { - return await MembershipModel.findOne({ + return await membershipRepo.findOne({ domain: ctx.subdomain._id, entityId: communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -648,7 +657,7 @@ export async function createCommunityPost({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -674,7 +683,7 @@ export async function createCommunityPost({ } } - const post = await CommunityPostModel.create({ + const post = await communityPostRepo.create({ domain: ctx.subdomain._id, userId: ctx.user.userId, communityId: community.communityId, @@ -743,7 +752,7 @@ export async function updateCommunityPost({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -757,7 +766,7 @@ export async function updateCommunityPost({ throw new Error(responses.action_not_allowed); } - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -840,7 +849,7 @@ export async function deleteCommunityPost({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -858,7 +867,7 @@ export async function deleteCommunityPost({ query["userId"] = ctx.user.userId; } - const post = await CommunityPostModel.findOne(query); + const post = await communityPostRepo.findOne(query); if (!post) { throw new Error(responses.item_not_found); @@ -903,14 +912,14 @@ export async function getPost({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); if (!community) { throw new Error(responses.item_not_found); } - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -955,7 +964,7 @@ export async function getPosts({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); if (!community) { @@ -983,7 +992,7 @@ export async function getPosts({ }); if (!category) { - const pinnedPosts = await CommunityPostModel.find({ + const pinnedPosts = await communityPostRepo.find({ domain: ctx.subdomain._id, communityId, pinned: true, @@ -1007,7 +1016,7 @@ export async function getFeed({ }): Promise<(PublicPost & { community: { id: string; title: string } })[]> { checkIfAuthenticated(ctx); - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: ctx.subdomain._id, userId: ctx.user.userId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -1022,7 +1031,7 @@ export async function getFeed({ return []; } - const communities = await CommunityModel.find({ + const communities = await communityRepo.find({ domain: ctx.subdomain._id, communityId: { $in: communityIds }, deleted: false, @@ -1079,7 +1088,7 @@ export async function getFeedCount({ }): Promise { checkIfAuthenticated(ctx); - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: ctx.subdomain._id, userId: ctx.user.userId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -1094,7 +1103,7 @@ export async function getFeedCount({ return 0; } - const communities = await CommunityModel.find({ + const communities = await communityRepo.find({ domain: ctx.subdomain._id, communityId: { $in: communityIds }, deleted: false, @@ -1118,7 +1127,7 @@ export async function getFeedCount({ return 0; } - return await CommunityPostModel.countDocuments({ + return await communityPostRepo.count({ domain: ctx.subdomain._id, communityId: { $in: visibleCommunityIds }, deleted: false, @@ -1136,7 +1145,7 @@ export async function getPostsCount({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1160,7 +1169,7 @@ export async function getPostsCount({ query.category = category; } - const count = await CommunityPostModel.countDocuments(query); + const count = await communityPostRepo.count(query); return count; } @@ -1174,7 +1183,7 @@ export async function getMember({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1182,7 +1191,7 @@ export async function getMember({ throw new Error(responses.item_not_found); } - const member = await MembershipModel.findOne({ + const member = await membershipRepo.findOne({ domain: ctx.subdomain._id, entityId: communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -1222,7 +1231,7 @@ export async function getMembers({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1310,7 +1319,7 @@ export async function getMembersCount({ query.status = status; } - const count = await (MembershipModel as any).countDocuments(query); + const count = await membershipRepo.count(query); return count; } @@ -1332,7 +1341,7 @@ export async function updateMemberStatus({ throw new Error(responses.action_not_allowed); } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1346,7 +1355,7 @@ export async function updateMemberStatus({ throw new Error(responses.item_not_found); } - const targetMember = await MembershipModel.findOne({ + const targetMember = await membershipRepo.findOne({ domain: ctx.subdomain._id, userId, entityId: communityId, @@ -1357,7 +1366,7 @@ export async function updateMemberStatus({ throw new Error(responses.item_not_found); } - const otherActiveModeratorsCount = await MembershipModel.countDocuments({ + const otherActiveModeratorsCount = await membershipRepo.count({ domain: ctx.subdomain._id, entityId: communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -1397,7 +1406,7 @@ export async function updateMemberStatus({ if (targetMember.status === Constants.MembershipStatus.ACTIVE) { targetMember.rejectionReason = undefined; - const paymentPlan = (await PaymentPlanModel.findOne({ + const paymentPlan = (await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, planId: targetMember.paymentPlanId, })) as PaymentPlan; @@ -1446,7 +1455,7 @@ export async function updateMemberRole({ throw new Error(responses.action_not_allowed); } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1466,7 +1475,7 @@ export async function updateMemberRole({ throw new Error(responses.item_not_found); } - const targetMember = await MembershipModel.findOne({ + const targetMember = await membershipRepo.findOne({ domain: ctx.subdomain._id, userId, entityId: communityId, @@ -1481,7 +1490,7 @@ export async function updateMemberRole({ throw new Error(responses.cannot_change_role_inactive_member); } - const otherActiveModeratorsCount = await MembershipModel.countDocuments({ + const otherActiveModeratorsCount = await membershipRepo.count({ domain: ctx.subdomain._id, entityId: communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -1516,7 +1525,7 @@ export async function togglePostLike({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1524,7 +1533,7 @@ export async function togglePostLike({ throw new Error(responses.item_not_found); } - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1582,7 +1591,7 @@ export async function togglePinned({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1590,7 +1599,7 @@ export async function togglePinned({ throw new Error(responses.item_not_found); } - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1637,7 +1646,7 @@ export async function postComment({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1664,7 +1673,7 @@ export async function postComment({ let comment; if (parentCommentId) { - comment = await CommunityCommentModel.findOne({ + comment = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1708,7 +1717,7 @@ export async function postComment({ }, }); } else { - comment = await CommunityCommentModel.create({ + comment = await communityCommentRepo.create({ domain: ctx.subdomain._id, userId: ctx.user.userId, communityId, @@ -1760,7 +1769,7 @@ export async function getComments({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1802,7 +1811,7 @@ export async function toggleCommentLike({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1810,7 +1819,7 @@ export async function toggleCommentLike({ throw new Error(responses.item_not_found); } - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1870,7 +1879,7 @@ export async function toggleCommentReplyLike({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1878,7 +1887,7 @@ export async function toggleCommentReplyLike({ throw new Error(responses.item_not_found); } - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1946,7 +1955,7 @@ export async function deleteComment({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -1954,7 +1963,7 @@ export async function deleteComment({ throw new Error(responses.item_not_found); } - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -1965,7 +1974,7 @@ export async function deleteComment({ throw new Error(responses.item_not_found); } - let comment = await CommunityCommentModel.findOne({ + let comment = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, postId, @@ -2027,15 +2036,13 @@ export async function leaveCommunity({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( - getCommunityQuery(ctx, id), - ); + const community = await communityRepo.findOne(getCommunityQuery(ctx, id)); if (!community) { throw new Error(responses.item_not_found); } - const member = await MembershipModel.findOne({ + const member = await membershipRepo.findOne({ domain: ctx.subdomain._id, userId: ctx.user.userId, entityId: id, @@ -2048,7 +2055,7 @@ export async function leaveCommunity({ } if (member.role === Constants.MembershipRole.MODERATE) { - const otherModeratorsCount = await MembershipModel.countDocuments({ + const otherModeratorsCount = await membershipRepo.count({ domain: ctx.subdomain._id, entityId: id, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -2087,7 +2094,7 @@ export async function leaveCommunity({ await member.deleteOne(); - const communityManagers: Membership[] = await MembershipModel.find({ + const communityManagers: Membership[] = await membershipRepo.find({ domain: ctx.subdomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -2257,7 +2264,7 @@ async function deleteCommunityPostsSubscriptions( } async function deleteMemberships(community: Community, ctx: GQLContext) { - const paymentPlans = await PaymentPlanModel.find({ + const paymentPlans = await paymentPlanRepo.find({ domain: ctx.subdomain._id, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -2282,7 +2289,7 @@ async function deleteMemberships(community: Community, ctx: GQLContext) { isIncludedInPlan: true, }); } - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ domain: ctx.subdomain._id, paymentPlanId: paymentPlan.planId, }); @@ -2342,7 +2349,7 @@ export async function reportCommunityContent({ }): Promise { checkIfAuthenticated(ctx); - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -2370,21 +2377,21 @@ export async function reportCommunityContent({ let content: any = undefined; if (type === Constants.CommunityReportType.POST) { - content = await CommunityPostModel.findOne({ + content = await communityPostRepo.findOne({ domain: ctx.subdomain._id, communityId, postId: contentId, deleted: false, }); } else if (type === Constants.CommunityReportType.COMMENT) { - content = await CommunityCommentModel.findOne({ + content = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, commentId: contentId, deleted: false, }); } else if (type === Constants.CommunityReportType.REPLY) { - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ domain: ctx.subdomain._id, communityId, commentId: contentParentId, @@ -2438,7 +2445,7 @@ export async function getCommunityReports({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -2484,7 +2491,7 @@ export async function getCommunityReportsCount({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); @@ -2529,7 +2536,7 @@ export async function updateCommunityReportStatus({ // throw new Error(responses.action_not_allowed); // } - const community = await CommunityModel.findOne( + const community = await communityRepo.findOne( getCommunityQuery(ctx, communityId), ); diff --git a/apps/web/graphql/courses/__tests__/delete-course.test.ts b/apps/web/graphql/courses/__tests__/delete-course.test.ts index fea023b93..712c5d244 100644 --- a/apps/web/graphql/courses/__tests__/delete-course.test.ts +++ b/apps/web/graphql/courses/__tests__/delete-course.test.ts @@ -21,6 +21,25 @@ import ProductDiscussionSubscriberModel from "@models/ProductDiscussionSubscribe import RateLimitEventModel from "@models/RateLimitEvent"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; +import { + CourseRepository, + PageRepository, + MembershipRepository, + UserRepository, + PaymentPlanRepository, + LessonRepository, + NotificationRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const notificationRepo = new NotificationRepository(NotificationModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit"); jest.mock("@/services/queue"); @@ -44,13 +63,13 @@ describe("deleteCourse - Comprehensive Test Suite", () => { beforeAll(async () => { // Create unique test domain - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: dcId("domain"), email: dcEmail("domain"), }); // Create admin user with course management permissions - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: dcId("admin-user"), email: dcEmail("admin"), @@ -62,7 +81,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Create regular user (student) - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: dcId("regular-user"), email: dcEmail("regular"), @@ -75,7 +94,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { // Create internal payment plan (required for memberships) // Use unique planId to avoid conflicts when running tests in parallel - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: dcId("internal-plan"), userId: adminUser.userId, @@ -159,7 +178,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { subdomain: testDomain, } as any; - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-perm", name: "Test Page", @@ -167,7 +186,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-perm", title: "Test Course", @@ -195,7 +214,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); it("should allow owner with manageCourse permission to delete their own course", async () => { - const ownerUser = await UserModel.create({ + const ownerUser = await userRepo.create({ domain: testDomain._id, userId: dcId("owner-user"), email: dcEmail("owner"), @@ -211,7 +230,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { subdomain: testDomain, } as any; - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-owner", name: "Test Page", @@ -219,7 +238,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-owner", title: "Test Course", @@ -238,7 +257,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { const result = await deleteCourse(course.courseId, ownerCtx); expect(result).toBe(true); - const deletedCourse = await CourseModel.findOne({ + const deletedCourse = await courseRepo.findOne({ courseId: course.courseId, }); expect(deletedCourse).toBeNull(); @@ -249,7 +268,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Course Deletion", () => { it("should delete a basic course successfully", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-basic", name: "Test Page", @@ -257,7 +276,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-basic", title: "Test Course", @@ -277,14 +296,14 @@ describe("deleteCourse - Comprehensive Test Suite", () => { expect(result).toBe(true); // Verify course is deleted - const deletedCourse = await CourseModel.findOne({ + const deletedCourse = await courseRepo.findOne({ courseId: course.courseId, }); expect(deletedCourse).toBeNull(); // Verify page is deleted // Note: deletePageInternal removes the page if deleteable is true - const deletedPage = await PageModel.findOne({ + const deletedPage = await pageRepo.findOne({ pageId: page.pageId, }); expect(deletedPage).toBeNull(); @@ -296,14 +315,14 @@ describe("deleteCourse - Comprehensive Test Suite", () => { const commentId = dcId("discussion-comment"); const replyId = dcId("discussion-reply"); const unrelatedProductId = dcId("unrelated-product"); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: dcId("discussion-page"), name: "Discussion Course Page", creatorId: adminUser.userId, deleteable: true, }); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: productId, title: "Discussion Course", @@ -374,7 +393,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { entityId: commentId, metadata: { courseId: productId }, }), - NotificationModel.create({ + notificationRepo.create({ domain: testDomain._id, notificationId: dcId("discussion-notification"), userId: regularUser.userId, @@ -393,7 +412,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { entityId: dcId("unrelated-comment"), metadata: { courseId: unrelatedProductId }, }), - NotificationModel.create({ + notificationRepo.create({ domain: testDomain._id, notificationId: dcId("unrelated-notification"), userId: regularUser.userId, @@ -426,7 +445,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { domain: testDomain._id, "metadata.courseId": productId, }), - NotificationModel.countDocuments({ + notificationRepo.count({ domain: testDomain._id, "metadata.courseId": productId, }), @@ -439,7 +458,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { domain: testDomain._id, "metadata.courseId": unrelatedProductId, }), - NotificationModel.countDocuments({ + notificationRepo.count({ domain: testDomain._id, "metadata.courseId": unrelatedProductId, }), @@ -450,7 +469,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { it("should delete course with featured image", async () => { const { deleteMedia } = require("@/services/medialit"); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-media", name: "Test Page", @@ -458,7 +477,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-media", title: "Test Course", @@ -489,7 +508,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); it("should delete course with description media", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-desc-media", name: "Test Page", @@ -497,7 +516,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-desc-media", title: "Test Course", @@ -531,7 +550,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Lesson Deletion", () => { it("should delete all lessons associated with the course", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-lessons", name: "Test Page", @@ -539,7 +558,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-lessons", title: "Test Course", @@ -562,7 +581,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: ["lesson-1", "lesson-2"], }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-1", title: "Lesson 1", @@ -574,7 +593,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { content: {}, }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-2", title: "Lesson 2", @@ -588,14 +607,14 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await deleteCourse(course.courseId, mockCtx); - const remainingLessons = await LessonModel.find({ + const remainingLessons = await lessonRepo.find({ courseId: course.courseId, }); expect(remainingLessons).toHaveLength(0); }); it("should delete lesson evaluations when deleting lessons", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-evals", name: "Test Page", @@ -603,7 +622,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-evals", title: "Test Course", @@ -626,7 +645,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: ["lesson-quiz"], }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-quiz", title: "Quiz Lesson", @@ -657,7 +676,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Certificate Deletion", () => { it("should delete certificate template and all issued certificates", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-certs", name: "Test Page", @@ -665,7 +684,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-certs", title: "Test Course", @@ -722,7 +741,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { it("should delete certificate template media", async () => { const { deleteMedia } = require("@/services/medialit"); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-cert-media", name: "Test Page", @@ -730,7 +749,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-cert-media", title: "Test Course", @@ -782,7 +801,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Membership & Payment Deletion", () => { it("should delete all memberships for the course", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-memberships", name: "Test Page", @@ -790,7 +809,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-memberships", title: "Test Course", @@ -806,7 +825,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: [], }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-1", userId: regularUser.userId, @@ -817,7 +836,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { status: Constants.MembershipStatus.ACTIVE, }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-2", userId: adminUser.userId, @@ -830,7 +849,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await deleteCourse(course.courseId, mockCtx); - const remainingMemberships = await MembershipModel.find({ + const remainingMemberships = await membershipRepo.find({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, }); @@ -838,7 +857,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); it("should delete all payment plans for the course", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-plans", name: "Test Page", @@ -846,7 +865,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-plans", title: "Test Course", @@ -862,7 +881,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: [], }); - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-course-1", userId: adminUser.userId, @@ -875,7 +894,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { currencyISOCode: "USD", }); - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-course-2", userId: adminUser.userId, @@ -890,7 +909,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await deleteCourse(course.courseId, mockCtx); - const remainingPlans = await PaymentPlanModel.find({ + const remainingPlans = await paymentPlanRepo.find({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, }); @@ -898,7 +917,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); it("should remove course from included products in other payment plans", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-included", name: "Test Page", @@ -906,7 +925,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-included", title: "Test Course", @@ -923,7 +942,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Create a bundle payment plan that includes this course - const bundlePlan = await PaymentPlanModel.create({ + const bundlePlan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-bundle", userId: adminUser.userId, @@ -939,7 +958,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await deleteCourse(course.courseId, mockCtx); - const updatedPlan = await PaymentPlanModel.findOne({ + const updatedPlan = await paymentPlanRepo.findOne({ planId: bundlePlan.planId, }); expect(updatedPlan?.includedProducts).toEqual(["other-course-123"]); @@ -948,7 +967,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Activity and Notification Deletion", () => { it("should delete all activities and notifications related to the course", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-activities-notifications", name: "Test Page", @@ -956,7 +975,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-activities-notifications", title: "Test Course", @@ -1003,7 +1022,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Notification with entityId = courseId - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: dcId("notification-entity"), userId: regularUser.userId, @@ -1013,7 +1032,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Notification with metadata.courseId - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: dcId("notification-metadata"), userId: regularUser.userId, @@ -1026,7 +1045,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Notification with both - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: dcId("notification-both"), userId: regularUser.userId, @@ -1049,7 +1068,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); expect(remainingActivities).toHaveLength(0); - const remainingNotifications = await NotificationModel.find({ + const remainingNotifications = await notificationRepo.find({ domain: testDomain._id, $or: [ { entityId: course.courseId }, @@ -1062,7 +1081,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("User Purchase Cleanup", () => { it("should remove course from all user purchases", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-purchases", name: "Test Page", @@ -1070,7 +1089,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-purchases", title: "Test Course", @@ -1115,7 +1134,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await deleteCourse(course.courseId, mockCtx); - const updatedRegularUser = await UserModel.findOne({ + const updatedRegularUser = await userRepo.findOne({ userId: regularUser.userId, }); expect( @@ -1124,7 +1143,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { ), ).toBeFalsy(); - const updatedAdminUser = await UserModel.findOne({ + const updatedAdminUser = await userRepo.findOne({ userId: adminUser.userId, }); expect( @@ -1137,7 +1156,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Invoice Deletion", () => { it("should NOT delete invoices when deleting course (current behavior)", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-invoices", name: "Test Page", @@ -1145,7 +1164,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-invoices", title: "Test Course", @@ -1161,7 +1180,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: [], }); - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-invoice", userId: regularUser.userId, @@ -1209,7 +1228,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { it("should handle course with all entities in lifecycle", async () => { const { deleteMedia } = require("@/services/medialit"); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-complex", name: "Test Page", @@ -1217,7 +1236,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-complex", title: "Complex Course", @@ -1256,7 +1275,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Create lessons - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-1", title: "Lesson 1", @@ -1268,7 +1287,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { content: {}, }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-2", title: "Quiz", @@ -1308,7 +1327,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Create payment plan - await PaymentPlanModel.create({ + await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-complex", userId: adminUser.userId, @@ -1322,7 +1341,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); // Create membership - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-complex", userId: regularUser.userId, @@ -1380,11 +1399,11 @@ describe("deleteCourse - Comprehensive Test Suite", () => { // Verify everything is deleted expect( - await CourseModel.findOne({ courseId: course.courseId }), + await courseRepo.findOne({ courseId: course.courseId }), ).toBeNull(); - expect(await PageModel.findOne({ pageId: page.pageId })).toBeNull(); + expect(await pageRepo.findOne({ pageId: page.pageId })).toBeNull(); expect( - await LessonModel.find({ courseId: course.courseId }), + await lessonRepo.find({ courseId: course.courseId }), ).toHaveLength(0); expect( await CertificateTemplateModel.find({ @@ -1395,13 +1414,13 @@ describe("deleteCourse - Comprehensive Test Suite", () => { await CertificateModel.find({ courseId: course.courseId }), ).toHaveLength(0); expect( - await PaymentPlanModel.find({ + await paymentPlanRepo.find({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, }), ).toHaveLength(0); expect( - await MembershipModel.find({ + await membershipRepo.find({ entityId: course.courseId, entityType: Constants.MembershipEntityType.COURSE, }), @@ -1421,7 +1440,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }), ).toHaveLength(0); - const updatedUser = await UserModel.findOne({ + const updatedUser = await userRepo.findOne({ userId: regularUser.userId, }); const hasCoursePurchase = @@ -1444,7 +1463,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { cancel: mockCancel, }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-subscription", name: "Test Page", @@ -1452,7 +1471,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-subscription", title: "Subscription Course", @@ -1468,7 +1487,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { lessons: [], }); - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-sub", userId: regularUser.userId, @@ -1500,7 +1519,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { // Membership is deleted but subscription is not cancelled expect( - await MembershipModel.findOne({ + await membershipRepo.findOne({ membershipId: membership.membershipId, }), ).toBeNull(); @@ -1516,7 +1535,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { describe("Edge Cases", () => { it("should handle course without page gracefully", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-no-page", title: "Course Without Page", @@ -1540,7 +1559,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { }); it("should handle course without any related entities", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-empty", name: "Test Page", @@ -1548,7 +1567,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-empty", title: "Empty Course", @@ -1568,11 +1587,11 @@ describe("deleteCourse - Comprehensive Test Suite", () => { expect(result).toBe(true); expect( - await CourseModel.findOne({ courseId: course.courseId }), + await courseRepo.findOne({ courseId: course.courseId }), ).toBeNull(); // Page might be marked as deleted or removed depending on implementation - const deletedPage = await PageModel.findOne({ + const deletedPage = await pageRepo.findOne({ pageId: page.pageId, }); // Either page is deleted or marked as deleted @@ -1587,7 +1606,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { new Error("Media deletion failed"), ); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-media-error", name: "Test Page", @@ -1595,7 +1614,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { deleteable: true, }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "test-course-media-error", title: "Course with Media Error", @@ -1625,7 +1644,7 @@ describe("deleteCourse - Comprehensive Test Suite", () => { // Course should still be deleted expect( - await CourseModel.findOne({ courseId: course.courseId }), + await courseRepo.findOne({ courseId: course.courseId }), ).toBeNull(); }); }); diff --git a/apps/web/graphql/courses/__tests__/logic.test.ts b/apps/web/graphql/courses/__tests__/logic.test.ts index 89bfdc060..f90896da7 100644 --- a/apps/web/graphql/courses/__tests__/logic.test.ts +++ b/apps/web/graphql/courses/__tests__/logic.test.ts @@ -53,6 +53,19 @@ import { import { assertRateLimit } from "@/lib/assert-rate-limit"; import { recordActivity } from "@/lib/record-activity"; import { deleteMedia, sealMedia } from "@/services/medialit"; +import { + LessonRepository, + CourseRepository, + UserRepository, + DomainRepository, + PageRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const pageRepo = new PageRepository(PageModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit", () => ({ deleteMedia: jest.fn().mockResolvedValue(true), @@ -281,12 +294,12 @@ describe("product discussion validation foundation", () => { let learnerUser: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("discussion-domain"), email: email("discussion-domain"), }); - learnerUser = await UserModel.create({ + learnerUser = await userRepo.create({ domain: testDomain._id, userId: id("discussion-learner"), email: email("discussion-learner"), @@ -338,7 +351,7 @@ describe("product discussion validation foundation", () => { }); it("rejects lesson discussion writes when discussions are disabled", async () => { - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("discussion-course"), title: id("discussion-course-title"), @@ -360,7 +373,7 @@ describe("product discussion validation foundation", () => { published: true, discussions: false, }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: id("discussion-lesson"), title: "Discussion Lesson", @@ -464,7 +477,7 @@ describe("product discussion validation foundation", () => { }); it("rejects lesson discussions on unpublished courses for learners but allows for creators/admins", async () => { - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("discussion-course-unpublished"), title: id("discussion-course-unpublished-title"), @@ -486,7 +499,7 @@ describe("product discussion validation foundation", () => { published: false, discussions: true, }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: id("discussion-lesson-unpublished"), title: "Discussion Lesson", @@ -513,7 +526,7 @@ describe("product discussion validation foundation", () => { ).rejects.toThrow(responses.item_not_found); // 2. Creator (who is owner/creator of the course) should be allowed: - const creatorUser = await UserModel.create({ + const creatorUser = await userRepo.create({ domain: testDomain._id, userId: id("another-creator"), email: email("another-creator"), @@ -559,12 +572,12 @@ describe("product discussion comment and reply logic", () => { }); beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("discussion-api-domain"), email: email("discussion-api-domain"), }); - learnerUser = await UserModel.create({ + learnerUser = await userRepo.create({ domain: testDomain._id, userId: id("discussion-api-learner"), email: email("discussion-api-learner"), @@ -580,7 +593,7 @@ describe("product discussion comment and reply logic", () => { }, ], }); - secondLearnerUser = await UserModel.create({ + secondLearnerUser = await userRepo.create({ domain: testDomain._id, userId: id("discussion-api-second-learner"), email: email("discussion-api-second-learner"), @@ -596,7 +609,7 @@ describe("product discussion comment and reply logic", () => { }, ], }); - nonEnrolledUser = await UserModel.create({ + nonEnrolledUser = await userRepo.create({ domain: testDomain._id, userId: id("discussion-api-non-enrolled"), email: email("discussion-api-non-enrolled"), @@ -606,7 +619,7 @@ describe("product discussion comment and reply logic", () => { unsubscribeToken: id("discussion-api-non-enrolled-token"), purchases: [], }); - productAdminUser = await UserModel.create({ + productAdminUser = await userRepo.create({ domain: testDomain._id, userId: id("discussion-api-admin"), email: email("discussion-api-admin"), @@ -667,7 +680,7 @@ describe("product discussion comment and reply logic", () => { ]); recordActivityMock.mockClear(); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId, title: id("discussion-api-course-title"), @@ -689,7 +702,7 @@ describe("product discussion comment and reply logic", () => { published: true, discussions: true, }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Discussion API Lesson", @@ -1820,7 +1833,7 @@ describe("product discussion comment and reply logic", () => { }, }, ); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId: lockedLessonId, title: "Locked Discussion API Lesson", @@ -1946,13 +1959,13 @@ describe("updateCourse", () => { let page: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); // Create admin user with course management permissions - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin-user"), email: email("admin"), @@ -1963,7 +1976,7 @@ describe("updateCourse", () => { purchases: [], }); - page = await PageModel.create({ + page = await pageRepo.create({ domain: testDomain._id, pageId: "test-page-perm", name: "Test Page", @@ -2015,7 +2028,7 @@ describe("updateCourse", () => { ], }; - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-unique"), title: id("course-title"), @@ -2088,7 +2101,7 @@ describe("updateCourse", () => { ], }; - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-unique-2"), title: id("course-title-2"), @@ -2146,7 +2159,7 @@ describe("updateCourse", () => { }); it("updates one property on an incomplete draft blog", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("draft-blog"), title: id("draft-blog-title"), @@ -2178,7 +2191,7 @@ describe("updateCourse", () => { }); it("updates a draft blog description with serialized Tiptap content", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("draft-blog-description"), title: id("draft-blog-description-title"), @@ -2227,7 +2240,7 @@ describe("updateCourse", () => { }); it("validates the overall state when publishing an incomplete blog", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("publish-incomplete-blog"), title: id("publish-incomplete-blog-title"), @@ -2270,12 +2283,12 @@ describe("getCourse", () => { let ownerWithoutManagePermission: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: getCourseId("domain"), email: getCourseEmail("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: getCourseId("admin-user"), email: getCourseEmail("admin"), @@ -2286,7 +2299,7 @@ describe("getCourse", () => { purchases: [], }); - ownerManager = await UserModel.create({ + ownerManager = await userRepo.create({ domain: testDomain._id, userId: getCourseId("owner-manager"), email: getCourseEmail("owner-manager"), @@ -2297,7 +2310,7 @@ describe("getCourse", () => { purchases: [], }); - ownerWithoutManagePermission = await UserModel.create({ + ownerWithoutManagePermission = await userRepo.create({ domain: testDomain._id, userId: getCourseId("owner-without-manage"), email: getCourseEmail("owner-without-manage"), @@ -2323,7 +2336,7 @@ describe("getCourse", () => { const groupId1 = getCourseId("group-1"); const groupId2 = getCourseId("group-2"); const groupId3 = getCourseId("group-3"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: getCourseId("course"), title: getCourseId("course-title"), @@ -2378,7 +2391,7 @@ describe("getCourse", () => { }); it("does not allow course managers to preview unpublished courses without preview mode", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: getCourseId("unpublished-course-no-preview"), title: getCourseId("unpublished-course-no-preview-title"), @@ -2403,7 +2416,7 @@ describe("getCourse", () => { }); it("allows course managers to preview unpublished courses in preview mode", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: getCourseId("unpublished-course"), title: getCourseId("unpublished-course-title"), @@ -2434,7 +2447,7 @@ describe("getCourse", () => { }); it("returns non-preview course responses for managers outside preview mode", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: getCourseId("published-manager-course"), title: getCourseId("published-manager-course-title"), @@ -2460,7 +2473,7 @@ describe("getCourse", () => { }); it("does not allow course owners without manage permission to preview unpublished courses", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: getCourseId("unpublished-owner-course"), title: getCourseId("unpublished-owner-course-title"), @@ -2498,12 +2511,12 @@ describe("public API product read helpers", () => { `public-api-read-${Date.now()}-${suffix}`; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: helperId("domain"), email: `${helperId("domain")}@example.com`, }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: helperId("admin-user"), email: `${helperId("admin")}@example.com`, @@ -2565,7 +2578,7 @@ describe("public API product read helpers", () => { }); it("returns owned dashboard products for course:manage users", async () => { - const courseManageUser = await UserModel.create({ + const courseManageUser = await userRepo.create({ domain: testDomain._id, userId: helperId("dashboard-manage-user"), email: `${helperId("dashboard-manage")}@example.com`, @@ -2576,7 +2589,7 @@ describe("public API product read helpers", () => { purchases: [], }); - const ownedCourse = await CourseModel.create({ + const ownedCourse = await courseRepo.create({ domain: testDomain._id, courseId: helperId("dashboard-owned-course"), title: "Dashboard Owned Course", @@ -2590,7 +2603,7 @@ describe("public API product read helpers", () => { slug: helperId("dashboard-owned-course-slug"), }); - const otherCourse = await CourseModel.create({ + const otherCourse = await courseRepo.create({ domain: testDomain._id, courseId: helperId("dashboard-other-course"), title: "Dashboard Other Course", @@ -2624,7 +2637,7 @@ describe("public API product read helpers", () => { }); it("restricts overview metrics to owned products for course:manage users", async () => { - const courseManageUser = await UserModel.create({ + const courseManageUser = await userRepo.create({ domain: testDomain._id, userId: helperId("manage-user-2"), email: `${helperId("manage2")}@example.com`, @@ -2635,7 +2648,7 @@ describe("public API product read helpers", () => { purchases: [], }); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: helperId("owned-course"), title: "Owned Course", @@ -2649,7 +2662,7 @@ describe("public API product read helpers", () => { slug: helperId("owned-course-slug"), }); - const otherCourse = await CourseModel.create({ + const otherCourse = await courseRepo.create({ domain: testDomain._id, courseId: helperId("other-course"), title: "Other Course", @@ -2724,7 +2737,7 @@ describe("public API product read helpers", () => { }); it("rejects course-scoped lesson reads when the lesson belongs to another product", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: helperId("course"), title: "Course", @@ -2737,7 +2750,7 @@ describe("public API product read helpers", () => { cost: 0, slug: helperId("course-slug"), }); - const otherCourse = await CourseModel.create({ + const otherCourse = await courseRepo.create({ domain: testDomain._id, courseId: helperId("other-course"), title: "Other Course", @@ -2751,7 +2764,7 @@ describe("public API product read helpers", () => { slug: helperId("other-course-slug"), }); const lessonId = helperId("other-course-lesson"); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Other Course Lesson", @@ -2776,7 +2789,7 @@ describe("public API product read helpers", () => { }); it("filters product members by user name or email inside GraphQL logic", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: helperId("member-course"), title: "Course", @@ -2789,7 +2802,7 @@ describe("public API product read helpers", () => { cost: 0, slug: helperId("member-course-slug"), }); - const matchingByName = await UserModel.create({ + const matchingByName = await userRepo.create({ domain: testDomain._id, userId: helperId("matching-name-user"), email: `${helperId("matching-name")}@example.com`, @@ -2799,7 +2812,7 @@ describe("public API product read helpers", () => { unsubscribeToken: helperId("matching-name-unsubscribe"), purchases: [{ courseId: course.courseId, completedLessons: [] }], }); - const matchingByEmail = await UserModel.create({ + const matchingByEmail = await userRepo.create({ domain: testDomain._id, userId: helperId("matching-email-user"), email: `student-${helperId("matching-email")}@example.com`, @@ -2809,7 +2822,7 @@ describe("public API product read helpers", () => { unsubscribeToken: helperId("matching-email-unsubscribe"), purchases: [{ courseId: course.courseId, completedLessons: [] }], }); - const nonMatchingUser = await UserModel.create({ + const nonMatchingUser = await userRepo.create({ domain: testDomain._id, userId: helperId("non-matching-user"), email: `${helperId("other")}@example.com`, @@ -2888,12 +2901,12 @@ describe("updateDiscussionComment / updateDiscussionReply", () => { let otherCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: editId("domain"), email: editEmail("domain"), }); - authorUser = await UserModel.create({ + authorUser = await userRepo.create({ domain: testDomain._id, userId: editId("author"), email: editEmail("author"), @@ -2910,7 +2923,7 @@ describe("updateDiscussionComment / updateDiscussionReply", () => { ], }); - otherUser = await UserModel.create({ + otherUser = await userRepo.create({ domain: testDomain._id, userId: editId("other"), email: editEmail("other"), @@ -2927,7 +2940,7 @@ describe("updateDiscussionComment / updateDiscussionReply", () => { ], }); - course = await CourseModel.create({ + course = await courseRepo.create({ domain: testDomain._id, courseId: editId("course"), title: editId("course-title"), @@ -2950,7 +2963,7 @@ describe("updateDiscussionComment / updateDiscussionReply", () => { discussions: true, }); - lesson = await LessonModel.create({ + lesson = await lessonRepo.create({ domain: testDomain._id, lessonId: editId("lesson"), title: "Edit Lesson", diff --git a/apps/web/graphql/courses/__tests__/move-lesson.test.ts b/apps/web/graphql/courses/__tests__/move-lesson.test.ts index 832ad9056..1e8d9bc09 100644 --- a/apps/web/graphql/courses/__tests__/move-lesson.test.ts +++ b/apps/web/graphql/courses/__tests__/move-lesson.test.ts @@ -5,6 +5,17 @@ import LessonModel from "@models/Lesson"; import constants from "@/config/constants"; import { responses } from "@/config/strings"; import { moveLesson } from "../logic"; +import { + LessonRepository, + CourseRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `move-lesson-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -17,12 +28,12 @@ describe("moveLesson", () => { let otherUserWithManageCourse: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin-user"), email: email("admin"), @@ -33,7 +44,7 @@ describe("moveLesson", () => { purchases: [], }); - ownerWithManageCourse = await UserModel.create({ + ownerWithManageCourse = await userRepo.create({ domain: testDomain._id, userId: id("owner-manage-course"), email: email("owner-manage-course"), @@ -44,7 +55,7 @@ describe("moveLesson", () => { purchases: [], }); - otherUserWithManageCourse = await UserModel.create({ + otherUserWithManageCourse = await userRepo.create({ domain: testDomain._id, userId: id("other-manage-course"), email: email("other-manage-course"), @@ -73,7 +84,7 @@ describe("moveLesson", () => { const lesson1 = id("lesson-1"); const lesson2 = id("lesson-2"); const lesson3 = id("lesson-3"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-same-group"), title: id("course-title-same-group"), @@ -157,7 +168,7 @@ describe("moveLesson", () => { const groupId2 = id("group-2"); const lesson1 = id("lesson-1"); const lesson2 = id("lesson-2"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-cross-group"), title: id("course-title-cross-group"), @@ -238,7 +249,7 @@ describe("moveLesson", () => { it("rejects move when lesson id is not part of course.lessons", async () => { const groupId = id("group-1"); const lessonId = id("lesson-not-listed"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-invalid-membership"), title: id("course-title-invalid-membership"), @@ -260,7 +271,7 @@ describe("moveLesson", () => { slug: id("course-slug-invalid-membership"), }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Lesson 1", @@ -290,7 +301,7 @@ describe("moveLesson", () => { const groupId = id("group-1"); const unknownGroup = id("group-unknown"); const lessonId = id("lesson-1"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-unknown-group"), title: id("course-title-unknown-group"), @@ -312,7 +323,7 @@ describe("moveLesson", () => { slug: id("course-slug-unknown-group"), }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Lesson 1", @@ -340,7 +351,7 @@ describe("moveLesson", () => { it("rejects unknown lesson ids", async () => { const groupId = id("group-1"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-unknown-lesson"), title: id("course-title-unknown-lesson"), @@ -380,7 +391,7 @@ describe("moveLesson", () => { it("rejects unknown courses", async () => { const lessonId = id("lesson-for-missing-course"); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Lesson 1", @@ -410,7 +421,7 @@ describe("moveLesson", () => { const groupId1 = id("group-1"); const groupId2 = id("group-2"); const lessonId = id("lesson-1"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-duplicate-order"), title: id("course-title-duplicate-order"), @@ -439,7 +450,7 @@ describe("moveLesson", () => { slug: id("course-slug-duplicate-order"), }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Lesson 1", @@ -473,7 +484,7 @@ describe("moveLesson", () => { it("rejects non-owner users without manageAnyCourse permission", async () => { const groupId = id("group-1"); const lessonId = id("lesson-1"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-non-owner"), title: id("course-title-non-owner"), @@ -495,7 +506,7 @@ describe("moveLesson", () => { slug: id("course-slug-non-owner"), }); - await LessonModel.create({ + await lessonRepo.create({ domain: testDomain._id, lessonId, title: "Lesson 1", diff --git a/apps/web/graphql/courses/__tests__/reorder-groups.test.ts b/apps/web/graphql/courses/__tests__/reorder-groups.test.ts index d109b4635..97741f4f2 100644 --- a/apps/web/graphql/courses/__tests__/reorder-groups.test.ts +++ b/apps/web/graphql/courses/__tests__/reorder-groups.test.ts @@ -4,6 +4,15 @@ import CourseModel from "@models/Course"; import constants from "@/config/constants"; import { responses } from "@/config/strings"; import { reorderGroups } from "../logic"; +import { + CourseRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `reorder-groups-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -17,12 +26,12 @@ describe("reorderGroups", () => { let otherUserWithManageCourse: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin-user"), email: email("admin"), @@ -33,7 +42,7 @@ describe("reorderGroups", () => { purchases: [], }); - ownerWithManageCourse = await UserModel.create({ + ownerWithManageCourse = await userRepo.create({ domain: testDomain._id, userId: id("owner-manage-course"), email: email("owner-manage-course"), @@ -44,7 +53,7 @@ describe("reorderGroups", () => { purchases: [], }); - ownerWithoutManageCourse = await UserModel.create({ + ownerWithoutManageCourse = await userRepo.create({ domain: testDomain._id, userId: id("owner-without-manage-course"), email: email("owner-without-manage-course"), @@ -55,7 +64,7 @@ describe("reorderGroups", () => { purchases: [], }); - otherUserWithManageCourse = await UserModel.create({ + otherUserWithManageCourse = await userRepo.create({ domain: testDomain._id, userId: id("other-manage-course"), email: email("other-manage-course"), @@ -81,7 +90,7 @@ describe("reorderGroups", () => { const groupId1 = id("group-1"); const groupId2 = id("group-2"); const groupId3 = id("group-3"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course"), title: id("course-title"), @@ -154,7 +163,7 @@ describe("reorderGroups", () => { const groupId1 = id("group-dupe-1"); const groupId2 = id("group-dupe-2"); const groupId3 = id("group-dupe-3"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-dupe"), title: id("course-title-dupe"), @@ -207,7 +216,7 @@ describe("reorderGroups", () => { const groupId1 = id("group-count-1"); const groupId2 = id("group-count-2"); const groupId3 = id("group-count-3"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-count"), title: id("course-title-count"), @@ -261,7 +270,7 @@ describe("reorderGroups", () => { const groupId2 = id("group-unknown-2"); const groupId3 = id("group-unknown-3"); const unknownGroupId = id("group-unknown-missing"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-unknown"), title: id("course-title-unknown"), @@ -313,7 +322,7 @@ describe("reorderGroups", () => { it("allows owners with manageCourse and rejects owners without permissions", async () => { const groupId1 = id("group-owner-1"); const groupId2 = id("group-owner-2"); - const ownerCourse = await CourseModel.create({ + const ownerCourse = await courseRepo.create({ domain: testDomain._id, courseId: id("course-owner"), title: id("course-title-owner"), @@ -354,7 +363,7 @@ describe("reorderGroups", () => { }), ).resolves.toBeTruthy(); - const ownerNoPermissionCourse = await CourseModel.create({ + const ownerNoPermissionCourse = await courseRepo.create({ domain: testDomain._id, courseId: id("course-owner-no-permission"), title: id("course-title-owner-no-permission"), @@ -401,7 +410,7 @@ describe("reorderGroups", () => { it("rejects non-owner users with manageCourse", async () => { const groupId1 = id("group-non-owner-1"); const groupId2 = id("group-non-owner-2"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-non-owner"), title: id("course-title-non-owner"), diff --git a/apps/web/graphql/courses/__tests__/slug.test.ts b/apps/web/graphql/courses/__tests__/slug.test.ts index 7425b3d04..29137b62e 100644 --- a/apps/web/graphql/courses/__tests__/slug.test.ts +++ b/apps/web/graphql/courses/__tests__/slug.test.ts @@ -8,6 +8,17 @@ import PageModel from "@models/Page"; import DomainModel from "@models/Domain"; import UserModel from "@models/User"; import constants from "@/config/constants"; +import { + PageRepository, + CourseRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const pageRepo = new PageRepository(PageModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit", () => ({ deleteMedia: jest.fn().mockResolvedValue(true), @@ -44,12 +55,12 @@ describe("Course Slug Tests", () => { let mockCtx: any; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: domain._id, userId: id("admin"), email: email("admin"), @@ -112,7 +123,7 @@ describe("Course Slug Tests", () => { it("should auto-suffix slug on page collision for courses", async () => { // Create a page that will collide - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "colliding-course", name: "Existing Page", @@ -142,13 +153,13 @@ describe("Course Slug Tests", () => { expect(updated.slug).toBe("new-course-slug"); - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ courseId: created.courseId, }); expect(course?.slug).toBe("new-course-slug"); expect(course?.pageId).toBe("new-course-slug"); - const page = await PageModel.findOne({ + const page = await pageRepo.findOne({ entityId: created.courseId, domain: domain._id, }); diff --git a/apps/web/graphql/courses/__tests__/update-group-drip.test.ts b/apps/web/graphql/courses/__tests__/update-group-drip.test.ts index 4354e04dc..18529cc4b 100644 --- a/apps/web/graphql/courses/__tests__/update-group-drip.test.ts +++ b/apps/web/graphql/courses/__tests__/update-group-drip.test.ts @@ -5,6 +5,15 @@ import constants from "@/config/constants"; import { responses } from "@/config/strings"; import { Constants } from "@courselit/common-models"; import { updateGroup } from "../logic"; +import { + CourseRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `update-group-drip-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -15,12 +24,12 @@ describe("updateGroup drip status updates", () => { let adminUser: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin-user"), email: email("admin"), @@ -44,7 +53,7 @@ describe("updateGroup drip status updates", () => { it("persists drip.status=false when disabling scheduled release", async () => { const groupId = id("group"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course"), title: id("course-title"), @@ -98,7 +107,7 @@ describe("updateGroup drip status updates", () => { it("rejects status-only drip updates when the group has no drip type yet", async () => { const groupId = id("group-without-drip"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course-without-drip"), title: id("course-title-without-drip"), @@ -145,7 +154,7 @@ describe("updateGroup drip status updates", () => { it("rejects relative-date drip when delayInMillis is missing", async () => { const groupId = id("group-no-delay"); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("course-no-delay"), title: id("course-title-no-delay"), @@ -188,7 +197,7 @@ describe("updateGroup drip status updates", () => { it("rejects exact-date drip when dateInUTC is missing", async () => { const groupId = id("group-no-date"); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("course-no-date"), title: id("course-title-no-date"), @@ -229,7 +238,7 @@ describe("updateGroup drip status updates", () => { it("clears dateInUTC when switching from exact-date to relative-date", async () => { const groupId = id("group-switch-to-relative"); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("course-switch-to-relative"), title: id("title-switch-to-relative"), @@ -286,7 +295,7 @@ describe("updateGroup drip status updates", () => { it("clears delayInMillis when switching from relative-date to exact-date", async () => { const groupId = id("group-switch-to-exact"); - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: id("course-switch-to-exact"), title: id("title-switch-to-exact"), diff --git a/apps/web/graphql/courses/__tests__/update-group-metadata.test.ts b/apps/web/graphql/courses/__tests__/update-group-metadata.test.ts index 17ffa674b..fbc63caff 100644 --- a/apps/web/graphql/courses/__tests__/update-group-metadata.test.ts +++ b/apps/web/graphql/courses/__tests__/update-group-metadata.test.ts @@ -3,6 +3,15 @@ import UserModel from "@models/User"; import CourseModel from "@models/Course"; import constants from "@/config/constants"; import { updateGroup } from "../logic"; +import { + CourseRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `update-group-metadata-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -13,12 +22,12 @@ describe("updateGroup metadata updates", () => { let adminUser: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin-user"), email: email("admin"), @@ -42,7 +51,7 @@ describe("updateGroup metadata updates", () => { it("updates group name, rank and collapsed without touching lessonsOrder", async () => { const groupId = id("group"); - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: id("course"), title: id("course-title"), diff --git a/apps/web/graphql/lessons/__tests__/scorm.test.ts b/apps/web/graphql/lessons/__tests__/scorm.test.ts index 591d8d4a5..87c559232 100644 --- a/apps/web/graphql/lessons/__tests__/scorm.test.ts +++ b/apps/web/graphql/lessons/__tests__/scorm.test.ts @@ -4,6 +4,17 @@ import UserModel from "@/models/User"; import CourseModel from "@/models/Course"; import DomainModel from "@/models/Domain"; import { Constants } from "@courselit/common-models"; +import { + UserRepository, + LessonRepository, + CourseRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `scorm-tests-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -18,14 +29,14 @@ describe("SCORM Logic Integration", () => { beforeAll(async () => { // Create Domain - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), features: [], }); // Create User - user = await UserModel.create({ + user = await userRepo.create({ domain: testDomain._id, userId: id("user"), email: email("user"), @@ -39,7 +50,7 @@ describe("SCORM Logic Integration", () => { const groupId = id("group-1"); // Create Course - course = await CourseModel.create({ + course = await courseRepo.create({ domain: testDomain._id, courseId: id("course"), title: "SCORM Course", @@ -67,7 +78,7 @@ describe("SCORM Logic Integration", () => { }); // Create SCORM Lesson - scormLesson = await LessonModel.create({ + scormLesson = await lessonRepo.create({ domain: testDomain._id, courseId: course.courseId, lessonId: id("lesson-scorm"), @@ -114,7 +125,7 @@ describe("SCORM Logic Integration", () => { beforeEach(async () => { // Reset user progress for the lesson - const u = await UserModel.findById(user._id); + const u = await userRepo.findById(user._id); const purchase = u!.purchases.find( (p: any) => p.courseId === course.courseId, ); @@ -143,7 +154,7 @@ describe("SCORM Logic Integration", () => { it("should fail if SCORM 1.2 status is incomplete", async () => { // Update user with incomplete status - const u = await UserModel.findById(user._id); + const u = await userRepo.findById(user._id); const purchase = u!.purchases.find( (p: any) => p.courseId === course.courseId, ); @@ -167,7 +178,7 @@ describe("SCORM Logic Integration", () => { it("should succeed if SCORM 1.2 status is completed", async () => { // Update user with completed status - const u = await UserModel.findById(user._id); + const u = await userRepo.findById(user._id); const purchase = u!.purchases.find( (p: any) => p.courseId === course.courseId, ); @@ -188,7 +199,7 @@ describe("SCORM Logic Integration", () => { expect(result).toBe(true); // Verify it was marked as completed in progress - const updatedUser = await UserModel.findById(user._id); + const updatedUser = await userRepo.findById(user._id); const p = updatedUser!.purchases.find( (p: any) => p.courseId === course.courseId, ); @@ -196,7 +207,7 @@ describe("SCORM Logic Integration", () => { }); it("should succeed if SCORM 2004 completion_status is completed", async () => { - const u = await UserModel.findById(user._id); + const u = await userRepo.findById(user._id); const purchase = u!.purchases.find( (p: any) => p.courseId === course.courseId, ); @@ -216,7 +227,7 @@ describe("SCORM Logic Integration", () => { }); it("should succeed via fallback if interaction data exists", async () => { - const u = await UserModel.findById(user._id); + const u = await userRepo.findById(user._id); const purchase = u!.purchases.find( (p: any) => p.courseId === course.courseId, ); diff --git a/apps/web/graphql/lessons/__tests__/visibility.test.ts b/apps/web/graphql/lessons/__tests__/visibility.test.ts index 379fe957d..aa4b0a427 100644 --- a/apps/web/graphql/lessons/__tests__/visibility.test.ts +++ b/apps/web/graphql/lessons/__tests__/visibility.test.ts @@ -13,6 +13,17 @@ import { } from "../logic"; import { responses } from "@/config/strings"; import { sealMedia } from "@/services/medialit"; +import { + UserRepository, + LessonRepository, + CourseRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/services/medialit", () => ({ deleteMedia: jest.fn(), @@ -47,13 +58,13 @@ describe("Lesson visibility and progress", () => { let otherManagerCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), features: [], }); - creator = await UserModel.create({ + creator = await userRepo.create({ domain: testDomain._id, userId: id("creator"), email: email("creator"), @@ -64,7 +75,7 @@ describe("Lesson visibility and progress", () => { purchases: [], }); - student = await UserModel.create({ + student = await userRepo.create({ domain: testDomain._id, userId: id("student"), email: email("student"), @@ -75,7 +86,7 @@ describe("Lesson visibility and progress", () => { purchases: [], }); - manageAnyAdmin = await UserModel.create({ + manageAnyAdmin = await userRepo.create({ domain: testDomain._id, userId: id("manage-any-admin"), email: email("manage-any-admin"), @@ -86,7 +97,7 @@ describe("Lesson visibility and progress", () => { purchases: [], }); - ownerManager = await UserModel.create({ + ownerManager = await userRepo.create({ domain: testDomain._id, userId: id("owner-manager"), email: email("owner-manager"), @@ -97,7 +108,7 @@ describe("Lesson visibility and progress", () => { purchases: [], }); - otherManager = await UserModel.create({ + otherManager = await userRepo.create({ domain: testDomain._id, userId: id("other-manager"), email: email("other-manager"), @@ -112,7 +123,7 @@ describe("Lesson visibility and progress", () => { quizGroupId = id("quiz-group"); quizDripGroupId = id("quiz-drip-group"); - course = await CourseModel.create({ + course = await courseRepo.create({ domain: testDomain._id, courseId: id("course"), title: "Visibility Course", @@ -139,7 +150,7 @@ describe("Lesson visibility and progress", () => { ], }); - quizCourse = await CourseModel.create({ + quizCourse = await courseRepo.create({ domain: testDomain._id, courseId: id("quiz-course"), title: "Quiz Visibility Course", @@ -177,7 +188,7 @@ describe("Lesson visibility and progress", () => { ], }); - publishedLessonOne = await LessonModel.create({ + publishedLessonOne = await lessonRepo.create({ domain: testDomain._id, courseId: course.courseId, lessonId: id("published-1"), @@ -193,7 +204,7 @@ describe("Lesson visibility and progress", () => { groupId, }); - unpublishedLesson = await LessonModel.create({ + unpublishedLesson = await lessonRepo.create({ domain: testDomain._id, courseId: course.courseId, lessonId: id("unpublished"), @@ -209,7 +220,7 @@ describe("Lesson visibility and progress", () => { groupId, }); - publishedLessonTwo = await LessonModel.create({ + publishedLessonTwo = await lessonRepo.create({ domain: testDomain._id, courseId: course.courseId, lessonId: id("published-2"), @@ -239,7 +250,7 @@ describe("Lesson visibility and progress", () => { passingGrade: 70, }; - unpublishedQuizLesson = await LessonModel.create({ + unpublishedQuizLesson = await lessonRepo.create({ domain: testDomain._id, courseId: quizCourse.courseId, lessonId: id("unpublished-quiz"), @@ -252,7 +263,7 @@ describe("Lesson visibility and progress", () => { groupId: quizGroupId, }); - dripQuizLesson = await LessonModel.create({ + dripQuizLesson = await lessonRepo.create({ domain: testDomain._id, courseId: quizCourse.courseId, lessonId: id("drip-quiz"), @@ -333,7 +344,7 @@ describe("Lesson visibility and progress", () => { }); beforeEach(async () => { - const freshStudent = await UserModel.findById(student._id); + const freshStudent = await userRepo.findById(student._id); const purchase = freshStudent?.purchases.find( (item: any) => item.courseId === course.courseId, ); diff --git a/apps/web/graphql/lessons/helpers.ts b/apps/web/graphql/lessons/helpers.ts index 9e7dbbf58..37c4ac4d5 100644 --- a/apps/web/graphql/lessons/helpers.ts +++ b/apps/web/graphql/lessons/helpers.ts @@ -5,6 +5,10 @@ import CourseModel from "../../models/Course"; import { Group, Question, Quiz } from "@courselit/common-models"; import mongoose from "mongoose"; import { LessonWithStringContent } from "./logic"; +import { CourseRepository } from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); + const { text, audio, video, pdf, embed, quiz, file } = constants; type LessonValidatorProps = Pick< @@ -89,7 +93,7 @@ export const getGroupedLessons = async ( publishedOnly: boolean = false, select?: Record, ): Promise => { - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ courseId: courseId, domain: domainId, }); @@ -203,7 +207,7 @@ export async function isPartOfDripGroup( lesson: Lesson, domain: mongoose.Types.ObjectId, ) { - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ courseId: lesson.courseId, domain, }); diff --git a/apps/web/graphql/lessons/logic.ts b/apps/web/graphql/lessons/logic.ts index 76df25959..f4159f5d0 100644 --- a/apps/web/graphql/lessons/logic.ts +++ b/apps/web/graphql/lessons/logic.ts @@ -30,7 +30,11 @@ import { import LessonEvaluation from "../../models/LessonEvaluation"; import { checkPermission, extractMediaIDs } from "@courselit/utils"; import { recordActivity } from "../../lib/record-activity"; -import { InternalCourse } from "@courselit/orm-models"; +import { + InternalCourse, + LessonRepository, + CourseRepository, +} from "@courselit/orm-models"; import CertificateModel from "../../models/Certificate"; import { error } from "@/services/logger"; import getDeletedMediaIds from "@/lib/get-deleted-media-ids"; @@ -39,6 +43,9 @@ import UserModel from "../../models/User"; import { replaceTempMediaWithSealedMediaInProseMirrorDoc } from "@/lib/replace-temp-media-with-sealed-media-in-prosemirror-doc"; import { canManageCourseInContext } from "../courses/permissions"; +const courseRepo = new CourseRepository(CourseModel); +const lessonRepo = new LessonRepository(LessonModel); + const { permissions, quiz, scorm } = constants; async function canManageLessonCourse(lesson: Lesson, ctx: GQLContext) { @@ -77,7 +84,7 @@ export const getLessonOrThrow = async ( query.courseId = options.courseId; } - const lesson = await LessonModel.findOne(query); + const lesson = await lessonRepo.findOne(query); if (!lesson) { throw new Error(responses.item_not_found); @@ -117,7 +124,7 @@ export const getLessonDetails = async ( if (courseId) { query.courseId = courseId; } - const lesson = await LessonModel.findOne(query); + const lesson = await lessonRepo.findOne(query); if (!lesson) { throw new Error(responses.item_not_found); @@ -202,7 +209,7 @@ export const createLesson = async ( lessonValidator(lessonData); try { - const course: InternalCourse | null = await CourseModel.findOne({ + const course: InternalCourse | null = await courseRepo.findOne({ courseId: lessonData.courseId, domain: ctx.subdomain._id, }); @@ -216,7 +223,7 @@ export const createLesson = async ( throw new Error(responses.group_not_found); } - const lesson = await LessonModel.create({ + const lesson = await lessonRepo.create({ domain: ctx.subdomain._id, title: lessonData.title, type: lessonData.type, @@ -425,7 +432,7 @@ export const getAllLessons = async ( }; export const deleteAllLessons = async (courseId: string, ctx: GQLContext) => { - const allLessons = await LessonModel.find({ + const allLessons = await lessonRepo.find({ domain: ctx.subdomain._id, courseId, }); @@ -440,7 +447,7 @@ export const markLessonCompleted = async ( ) => { checkIfAuthenticated(ctx); - const lesson = await LessonModel.findOne({ + const lesson = await lessonRepo.findOne({ domain: ctx.subdomain._id, lessonId, }); @@ -542,7 +549,7 @@ const checkAndRecordCourseCompletion = async ( courseId: string, ctx: GQLContext, ) => { - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ domain: ctx.subdomain._id, courseId, }); @@ -650,7 +657,7 @@ export const evaluateLesson = async ( ) => { checkIfAuthenticated(ctx); - const lesson = await LessonModel.findOne({ + const lesson = await lessonRepo.findOne({ domain: ctx.subdomain._id, lessonId, }); diff --git a/apps/web/graphql/mails/__tests__/logic.test.ts b/apps/web/graphql/mails/__tests__/logic.test.ts index f9bb4fd47..b7bb3b23b 100644 --- a/apps/web/graphql/mails/__tests__/logic.test.ts +++ b/apps/web/graphql/mails/__tests__/logic.test.ts @@ -17,6 +17,9 @@ import { updateEmailTemplate, } from "../logic"; import SequenceModel from "@/models/Sequence"; +import { DomainRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); const { permissions } = constants; @@ -25,7 +28,7 @@ describe("createEmailTemplate", () => { let ctx: GQLContext; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: `mail-template-domain-${Date.now()}-${Math.floor(Math.random() * 100000)}`, email: "owner@example.com", }); diff --git a/apps/web/graphql/mails/logic.ts b/apps/web/graphql/mails/logic.ts index 39782cd5b..678e76167 100644 --- a/apps/web/graphql/mails/logic.ts +++ b/apps/web/graphql/mails/logic.ts @@ -27,7 +27,11 @@ import MailRequestStatusModel, { } from "@models/MailRequestStatus"; import { getPlans } from "../paymentplans/logic"; import { activateMembership } from "@/app/api/payment/helpers"; -import { AdminSequence, InternalCourse } from "@courselit/orm-models"; +import { + AdminSequence, + InternalCourse, + UserRepository, +} from "@courselit/orm-models"; import { User } from "@courselit/common-models"; import EmailDeliveryModel from "@models/EmailDelivery"; import EmailEventModel from "@models/EmailEvent"; @@ -36,6 +40,8 @@ import { defaultEmail } from "./default-email"; import { sanitizeEmail } from "@/lib/sanitize-email"; import { isDuplicateKeyError } from "../pages/helpers"; +const userRepo = new UserRepository(UserModel); + const { permissions } = constants; const isDateInFuture = (timestamp?: number) => @@ -123,7 +129,7 @@ export async function createSubscription( ): Promise { try { const sanitizedEmail = sanitizeEmail(email); - let dbUser: User | null = await UserModel.findOne({ + let dbUser: User | null = await userRepo.findOne({ email: sanitizedEmail, domain: ctx.subdomain._id, }); @@ -651,7 +657,7 @@ export async function sendCourseOverMail( throw new Error(responses.item_not_found); } - let dbUser: User | null = await UserModel.findOne({ + let dbUser: User | null = await userRepo.findOne({ email, domain: ctx.subdomain._id, }); @@ -1095,7 +1101,7 @@ export async function getSubscribersCount({ throw new Error(responses.item_not_found); } - const count = await UserModel.countDocuments({ + const count = await userRepo.count({ domain: ctx.subdomain._id, userId: { $in: sequence.entrants }, }); diff --git a/apps/web/graphql/menus/logic.ts b/apps/web/graphql/menus/logic.ts index 03c7ec553..2dfba4f4b 100644 --- a/apps/web/graphql/menus/logic.ts +++ b/apps/web/graphql/menus/logic.ts @@ -6,6 +6,10 @@ import DomainModel, { Domain } from "../../models/Domain"; import { responses } from "../../config/strings"; import constants from "../../config/constants"; import { checkPermission } from "@courselit/utils"; +import { DomainRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); + const { permissions } = constants; type DomainWithLinks = Domain & @@ -23,7 +27,7 @@ export const saveLink = async ( throw new Error(responses.action_not_allowed); } - const domain = (await DomainModel.findById( + const domain = (await domainRepo.findById( ctx.subdomain._id, )) as DomainWithLinks | null; if (!domain) { @@ -70,7 +74,7 @@ export const deleteLink = async ( throw new Error(responses.action_not_allowed); } - const domain = (await DomainModel.findById( + const domain = (await domainRepo.findById( ctx.subdomain._id, )) as DomainWithLinks | null; if (!domain) { diff --git a/apps/web/graphql/notifications/__tests__/logic.test.ts b/apps/web/graphql/notifications/__tests__/logic.test.ts index a73e4873b..448176da8 100644 --- a/apps/web/graphql/notifications/__tests__/logic.test.ts +++ b/apps/web/graphql/notifications/__tests__/logic.test.ts @@ -14,6 +14,21 @@ import CourseModel from "@models/Course"; import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; import { DiscussionActivityEventType } from "@/graphql/product-discussions/logic"; +import { + NotificationRepository, + CourseRepository, + CommunityPostRepository, + CommunityRepository, + UserRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const notificationRepo = new NotificationRepository(NotificationModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `notification-preferences-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -25,12 +40,12 @@ describe("Notification Preferences", () => { let manager: any; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: id("domain"), email: email("domain"), }); - learner = await UserModel.create({ + learner = await userRepo.create({ domain: domain._id, userId: id("learner"), email: email("learner"), @@ -41,7 +56,7 @@ describe("Notification Preferences", () => { unsubscribeToken: id("unsub-learner"), }); - manager = await UserModel.create({ + manager = await userRepo.create({ domain: domain._id, userId: id("manager"), email: email("manager"), @@ -271,7 +286,7 @@ describe("Notification Preferences", () => { }); it("should format notification message and href using shared formatter", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: domain._id, communityId: id("community"), name: "Community A", @@ -279,7 +294,7 @@ describe("Notification Preferences", () => { slug: id("community-page"), }); - const post = await CommunityPostModel.create({ + const post = await communityPostRepo.create({ domain: domain._id, postId: id("post"), communityId: community.communityId, @@ -289,7 +304,7 @@ describe("Notification Preferences", () => { category: "General", }); - const notification = await NotificationModel.create({ + const notification = await notificationRepo.create({ domain: domain._id, notificationId: id("notification"), userId: learner.userId, @@ -315,7 +330,7 @@ describe("Notification Preferences", () => { }); it("should add preview mode to unpublished course discussion notification hrefs for managers only", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: domain._id, courseId: id("discussion-course"), title: "Discussion Course", @@ -329,7 +344,7 @@ describe("Notification Preferences", () => { discussions: true, }); - const notification = await NotificationModel.create({ + const notification = await notificationRepo.create({ domain: domain._id, notificationId: id("discussion-notification"), userId: learner.userId, @@ -362,7 +377,7 @@ describe("Notification Preferences", () => { ); expect(response?.message).toContain("commented on Discussion Course"); - const learnerNotification = await NotificationModel.create({ + const learnerNotification = await notificationRepo.create({ domain: domain._id, notificationId: id("learner-discussion-notification"), userId: manager.userId, @@ -386,7 +401,7 @@ describe("Notification Preferences", () => { )}?discussion=open#discussion-comment-${id("comment")}`, ); - const reactionNotification = await NotificationModel.create({ + const reactionNotification = await notificationRepo.create({ domain: domain._id, notificationId: id("discussion-reaction-notification"), userId: learner.userId, @@ -421,7 +436,7 @@ describe("Notification Preferences", () => { }); it("should return empty message and href when entity cannot be resolved", async () => { - const notification = await NotificationModel.create({ + const notification = await notificationRepo.create({ domain: domain._id, notificationId: id("missing-entity-notification"), userId: learner.userId, @@ -445,7 +460,7 @@ describe("Notification Preferences", () => { it("should require activityType on notification documents", async () => { await expect( - NotificationModel.create({ + notificationRepo.create({ domain: domain._id, notificationId: id("invalid-notification"), userId: learner.userId, diff --git a/apps/web/graphql/notifications/logic.ts b/apps/web/graphql/notifications/logic.ts index 56aa291d6..9048f965f 100644 --- a/apps/web/graphql/notifications/logic.ts +++ b/apps/web/graphql/notifications/logic.ts @@ -17,6 +17,10 @@ import { getAllowedActivityTypesForPermissions, isActivityAllowedForPermissions, } from "./helpers"; +import { UserRepository, NotificationRepository } from "@courselit/orm-models"; + +const notificationRepo = new NotificationRepository(NotificationModel); +const userRepo = new UserRepository(UserModel); interface NotificationDocument { notificationId: string; @@ -242,7 +246,7 @@ export async function getNotifications({ .skip(skip) .limit(safeLimit) .lean(), - NotificationModel.countDocuments(query), + notificationRepo.count(query), ]); if (!notifications.length) { @@ -287,7 +291,7 @@ async function formatNotification( } async function getUserName(userId: string): Promise { - const user = await UserModel.findOne({ userId }); + const user = await userRepo.findOne({ userId }); return user?.name || user?.email || "Someone"; } diff --git a/apps/web/graphql/pages/__tests__/logic.test.ts b/apps/web/graphql/pages/__tests__/logic.test.ts index b27f9abae..23a01787f 100644 --- a/apps/web/graphql/pages/__tests__/logic.test.ts +++ b/apps/web/graphql/pages/__tests__/logic.test.ts @@ -11,6 +11,17 @@ import constants from "@/config/constants"; import { deleteMedia, sealMedia } from "@/services/medialit"; import GQLContext from "@/models/GQLContext"; import { responses } from "@/config/strings"; +import { + PageRepository, + DomainRepository, + CommunityRepository, + CourseRepository, +} from "@courselit/orm-models"; + +const communityRepo = new CommunityRepository(CommunityModel); +const courseRepo = new CourseRepository(Course); +const domainRepo = new DomainRepository(DomainModel); +const pageRepo = new PageRepository(PageModel); jest.mock("@/services/medialit", () => ({ deleteMedia: jest.fn().mockResolvedValue(true), @@ -60,7 +71,7 @@ describe("updatePage media handling", () => { const orphanUrl = "https://cdn.test/uploads/orphan-block/main.png"; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: `protected-media-domain-${Date.now()}-${Math.floor(Math.random() * 100000)}`, email: "owner@test.com", sharedWidgets: {}, @@ -94,7 +105,7 @@ describe("updatePage media handling", () => { }); it("skips deleting media that still exists in the published layout", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "layout-protection", type: constants.site, @@ -164,7 +175,7 @@ describe("updatePage media handling", () => { }); it("still deletes media that is no longer referenced anywhere", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "layout-cleanup", type: constants.site, @@ -203,7 +214,7 @@ describe("updatePage media handling", () => { }); it("throws when requester lacks permission", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "permission-check", type: constants.site, @@ -241,7 +252,7 @@ describe("getPage entity validation", () => { let ctx: GQLContext; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: `entity-validation-domain-${Date.now()}-${Math.floor(Math.random() * 100000)}`, email: "owner@test.com", sharedWidgets: {}, @@ -279,7 +290,7 @@ describe("getPage entity validation", () => { it("returns the page when course exists and is published", async () => { const courseId = "test-course-id"; - await Course.create({ + await courseRepo.create({ courseId, domain: ctx.subdomain._id, published: true, @@ -292,7 +303,7 @@ describe("getPage entity validation", () => { cost: 0, }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "product-page-1", type: constants.product, @@ -309,7 +320,7 @@ describe("getPage entity validation", () => { }); it("returns undefined when course does not exist", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "product-page-2", type: constants.product, @@ -327,7 +338,7 @@ describe("getPage entity validation", () => { it("returns undefined when course exists but is not published", async () => { const courseId = "unpublished-course-id"; - await Course.create({ + await courseRepo.create({ courseId, domain: ctx.subdomain._id, published: false, @@ -340,7 +351,7 @@ describe("getPage entity validation", () => { cost: 0, }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "product-page-3", type: constants.product, @@ -356,7 +367,7 @@ describe("getPage entity validation", () => { }); it("returns undefined when course belongs to different domain", async () => { - const otherDomain = await DomainModel.create({ + const otherDomain = await domainRepo.create({ name: "other-domain", email: "other@test.com", sharedWidgets: {}, @@ -365,7 +376,7 @@ describe("getPage entity validation", () => { const courseId = "different-domain-course"; - await Course.create({ + await courseRepo.create({ courseId, domain: otherDomain._id, published: true, @@ -378,7 +389,7 @@ describe("getPage entity validation", () => { cost: 0, }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "product-page-4", type: constants.product, @@ -402,7 +413,7 @@ describe("getPage entity validation", () => { it("returns the page when community exists and is enabled", async () => { const communityId = "test-community-id"; - await CommunityModel.create({ + await communityRepo.create({ communityId, domain: ctx.subdomain._id, enabled: true, @@ -411,7 +422,7 @@ describe("getPage entity validation", () => { slug: "community-page-1", }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "community-page-1", type: constants.communityPage, @@ -428,7 +439,7 @@ describe("getPage entity validation", () => { }); it("returns undefined when community does not exist", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "community-page-2", type: constants.communityPage, @@ -446,7 +457,7 @@ describe("getPage entity validation", () => { it("returns undefined when community exists but is not enabled", async () => { const communityId = "disabled-community-id"; - await CommunityModel.create({ + await communityRepo.create({ communityId, domain: ctx.subdomain._id, enabled: false, @@ -455,7 +466,7 @@ describe("getPage entity validation", () => { slug: "community-page-3", }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "community-page-3", type: constants.communityPage, @@ -471,7 +482,7 @@ describe("getPage entity validation", () => { }); it("returns undefined when community belongs to different domain", async () => { - const otherDomain = await DomainModel.create({ + const otherDomain = await domainRepo.create({ name: "other-community-domain", email: "other-community@test.com", sharedWidgets: {}, @@ -480,7 +491,7 @@ describe("getPage entity validation", () => { const communityId = "different-domain-community"; - await CommunityModel.create({ + await communityRepo.create({ communityId, domain: otherDomain._id, enabled: true, @@ -489,7 +500,7 @@ describe("getPage entity validation", () => { slug: "community-page-4", }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "community-page-4", type: constants.communityPage, @@ -513,7 +524,7 @@ describe("getPage entity validation", () => { it("admin can view unpublished product page", async () => { const courseId = "admin-course-id"; - await Course.create({ + await courseRepo.create({ courseId, domain: ctx.subdomain._id, published: false, @@ -526,7 +537,7 @@ describe("getPage entity validation", () => { cost: 0, }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "admin-product-page", type: constants.product, @@ -554,7 +565,7 @@ describe("getPage entity validation", () => { it("admin can view disabled community page", async () => { const communityId = "admin-community-id"; - await CommunityModel.create({ + await communityRepo.create({ communityId, domain: ctx.subdomain._id, enabled: false, @@ -563,7 +574,7 @@ describe("getPage entity validation", () => { slug: "admin-community-page", }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "admin-community-page", type: constants.communityPage, @@ -615,7 +626,7 @@ describe("Media cleanup", () => { }; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: `media-cleanup-domain-${Date.now()}-${Math.floor(Math.random() * 100000)}`, email: "owner@test.com", sharedWidgets: {}, @@ -645,7 +656,7 @@ describe("Media cleanup", () => { it("updating title will not call deleteMedia", async () => { // Setup: Page with media in draft layout - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "bug-partial-update", type: constants.site, @@ -680,7 +691,7 @@ describe("Media cleanup", () => { it("orphans social image when replaced", async () => { // Setup: Page with social image ONLY in draft - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "bug-social-image-orphan", type: constants.site, @@ -705,7 +716,7 @@ describe("Media cleanup", () => { it("existing social image is deleted when publishing", async () => { // Setup: Page with media1 as socialImage, and layout awaiting publish with media2 as new socialImage - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "bug-publish-social-orphan", type: constants.site, @@ -726,7 +737,7 @@ describe("Media cleanup", () => { describe("updatePage media cleanup", () => { it("deletes media removed from draft layout when not in published layout", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-removes-media", type: constants.site, @@ -759,7 +770,7 @@ describe("Media cleanup", () => { }); it("does NOT delete media still present in published layout", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-protects-published", type: constants.site, @@ -802,7 +813,7 @@ describe("Media cleanup", () => { }); it("deletes old draftSocialImage when replaced with new one", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-replaces-social", type: constants.site, @@ -823,7 +834,7 @@ describe("Media cleanup", () => { }); it("does NOT delete draftSocialImage if it is the same as published socialImage", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-protects-published-social", type: constants.site, @@ -845,7 +856,7 @@ describe("Media cleanup", () => { }); it("sets draftSocialImage to null when socialImage is null", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-clears-draft-social-image", type: constants.site, @@ -862,7 +873,7 @@ describe("Media cleanup", () => { socialImage: null, }); - const refreshedPage = await PageModel.findOne({ + const refreshedPage = await pageRepo.findOne({ _id: page._id, }); @@ -871,7 +882,7 @@ describe("Media cleanup", () => { }); it("does NOT delete the cleared draftSocialImage if published socialImage still uses it", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-clears-draft-but-protects-published-social", type: constants.site, @@ -889,7 +900,7 @@ describe("Media cleanup", () => { socialImage: null, }); - const refreshedPage = await PageModel.findOne({ + const refreshedPage = await pageRepo.findOne({ _id: page._id, }); @@ -898,7 +909,7 @@ describe("Media cleanup", () => { }); it("does not delete the existing draftSocialImage when sealing the new social image fails", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-social-image-seal-failure", type: constants.site, @@ -923,7 +934,7 @@ describe("Media cleanup", () => { expect(deleteMedia).not.toHaveBeenCalledWith(media1); - const refreshedPage = await PageModel.findOne({ + const refreshedPage = await pageRepo.findOne({ _id: page._id, }); @@ -931,7 +942,7 @@ describe("Media cleanup", () => { }); it("handles multiple media in a single widget", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-multiple-media", type: constants.site, @@ -975,7 +986,7 @@ describe("Media cleanup", () => { }); it("handles deeply nested media in widget settings", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-nested-media", type: constants.site, @@ -1018,7 +1029,7 @@ describe("Media cleanup", () => { }); it("does not delete any media when updating only metadata (title/description)", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "update-metadata-only", type: constants.site, @@ -1061,7 +1072,7 @@ describe("Media cleanup", () => { describe("publish media cleanup", () => { it("deletes media from old published layout not in new published layout", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-removes-media", type: constants.site, @@ -1087,7 +1098,7 @@ describe("Media cleanup", () => { }); it("does NOT delete media that exists in both old and new layouts", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-keeps-shared", type: constants.site, @@ -1131,7 +1142,7 @@ describe("Media cleanup", () => { }); it("deletes old socialImage when replaced during publish", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-replaces-social", type: constants.site, @@ -1150,7 +1161,7 @@ describe("Media cleanup", () => { }); it("does NOT delete socialImage if it still exists in draftLayout", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-social-in-layout", type: constants.site, @@ -1178,7 +1189,7 @@ describe("Media cleanup", () => { }); it("clears published socialImage when draftSocialImage is null", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-clears-social-image", type: constants.site, @@ -1192,7 +1203,7 @@ describe("Media cleanup", () => { await publish(page.pageId, ctx); - const refreshedPage = await PageModel.findOne({ + const refreshedPage = await pageRepo.findOne({ _id: page._id, }); @@ -1204,7 +1215,7 @@ describe("Media cleanup", () => { // Note: When draftLayout is empty, the layout is NOT copied (checked via `if (page.draftLayout.length)`) // However, the media diff computation still happens: currentPublished - nextPublished // With empty draftLayout, nextPublishedMedia is empty, so ALL currentPublishedMedia is deleted - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-empty-layouts", type: constants.site, @@ -1231,7 +1242,7 @@ describe("Media cleanup", () => { }); it("deletes multiple media removed during publish", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "publish-removes-multiple", type: constants.site, @@ -1278,7 +1289,7 @@ describe("Media cleanup", () => { describe("edge cases", () => { it("handles widget with no media settings", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "no-media-widget", type: constants.site, @@ -1309,7 +1320,7 @@ describe("Media cleanup", () => { }); it("handles media URL with different file extensions", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "media-extensions", type: constants.site, @@ -1350,7 +1361,7 @@ describe("Media cleanup", () => { }); it("does not crash when draftLayout is empty array", async () => { - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "empty-draft-layout", type: constants.site, @@ -1383,7 +1394,7 @@ describe("Media cleanup", () => { it("correctly identifies media IDs from complex URLs", async () => { const complexMediaId = "abc123def456"; - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: "complex-url", type: constants.site, diff --git a/apps/web/graphql/pages/__tests__/slug.test.ts b/apps/web/graphql/pages/__tests__/slug.test.ts index 449a8fe89..da18812f6 100644 --- a/apps/web/graphql/pages/__tests__/slug.test.ts +++ b/apps/web/graphql/pages/__tests__/slug.test.ts @@ -9,6 +9,10 @@ import { } from "../helpers"; import PageModel from "@models/Page"; import DomainModel from "@models/Domain"; +import { PageRepository, DomainRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); +const pageRepo = new PageRepository(PageModel); jest.mock("@/services/queue"); jest.mock("nanoid", () => ({ @@ -34,7 +38,7 @@ describe("Slug helpers", () => { let domain: any; beforeAll(async () => { - domain = await DomainModel.create({ + domain = await domainRepo.create({ name: id("domain"), email: `${id("domain")}@example.com`, }); @@ -89,7 +93,7 @@ describe("Slug helpers", () => { }); it("should append -1 suffix on first collision", async () => { - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "colliding-title", name: "Colliding Title", @@ -104,19 +108,19 @@ describe("Slug helpers", () => { }); it("should increment suffix on multiple collisions", async () => { - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "multi-collide", name: "Multi Collide", creatorId: "creator-1", }); - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "multi-collide-1", name: "Multi Collide 1", creatorId: "creator-1", }); - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "multi-collide-2", name: "Multi Collide 2", @@ -131,7 +135,7 @@ describe("Slug helpers", () => { }); it("should throw on collision when useSuffixOnCollision=false", async () => { - await PageModel.create({ + await pageRepo.create({ domain: domain._id, pageId: "no-suffix", name: "No Suffix", @@ -144,12 +148,12 @@ describe("Slug helpers", () => { }); it("should not collide with pages from different domains", async () => { - const otherDomain = await DomainModel.create({ + const otherDomain = await domainRepo.create({ name: id("other-domain"), email: `${id("other-domain")}@example.com`, }); - await PageModel.create({ + await pageRepo.create({ domain: otherDomain._id, pageId: "cross-domain", name: "Cross Domain", diff --git a/apps/web/graphql/pages/helpers.ts b/apps/web/graphql/pages/helpers.ts index 754f77a14..8aeaa5948 100644 --- a/apps/web/graphql/pages/helpers.ts +++ b/apps/web/graphql/pages/helpers.ts @@ -8,6 +8,9 @@ import { generateUniqueId, slugify } from "@courselit/utils"; import { getPlans } from "../paymentplans/logic"; import mongoose from "mongoose"; import { responses } from "../../config/strings"; +import { PageRepository } from "@courselit/orm-models"; + +const pageRepo = new PageRepository(PageModel); const MAX_SLUG_ATTEMPTS = 100; const MAX_SLUG_LENGTH = 200; @@ -46,7 +49,7 @@ export async function generateUniquePageId( let suffix = 0; while (suffix < MAX_SLUG_ATTEMPTS) { - const existing = await PageModel.findOne({ + const existing = await pageRepo.findOne({ domain: domainId, pageId: candidate, }); diff --git a/apps/web/graphql/pages/logic.ts b/apps/web/graphql/pages/logic.ts index 9f306f5f0..bac2892af 100644 --- a/apps/web/graphql/pages/logic.ts +++ b/apps/web/graphql/pages/logic.ts @@ -21,6 +21,16 @@ import getDeletedMediaIds from "@/lib/get-deleted-media-ids"; import { deleteMedia, sealMedia } from "@/services/medialit"; import CommunityModel from "@models/Community"; import { replaceTempMediaWithSealedMediaInPageLayout } from "@/lib/replace-temp-media-with-sealed-media-in-page-layout"; +import { + PageRepository, + CommunityRepository, + CourseRepository, +} from "@courselit/orm-models"; + +const communityRepo = new CommunityRepository(CommunityModel); +const courseRepo = new CourseRepository(Course); +const pageRepo = new PageRepository(PageModel); + const { product, site, blogPage, communityPage, permissions, defaultPages } = constants; const { pageNames } = Constants; @@ -95,7 +105,7 @@ export async function getPage({ if (!page) return; if (page.type === product) { - const course = await Course.findOne({ + const course = await courseRepo.findOne({ courseId: page.entityId, domain: ctx.subdomain._id, published: true, @@ -106,7 +116,7 @@ export async function getPage({ } if (page.type === communityPage) { - const community = await CommunityModel.findOne({ + const community = await communityRepo.findOne({ domain: ctx.subdomain._id, communityId: page.entityId, enabled: true, @@ -141,7 +151,7 @@ export const updatePage = async ({ if (!checkPermission(ctx.user.permissions, [permissions.manageSite])) { throw new Error(responses.action_not_allowed); } - const page: Page | null = await PageModel.findOne({ + const page: Page | null = await pageRepo.findOne({ pageId, domain: ctx.subdomain._id, }); @@ -246,7 +256,7 @@ export const publish = async ( if (!checkPermission(ctx.user.permissions, [permissions.manageSite])) { throw new Error(responses.action_not_allowed); } - const page: Page | null = await PageModel.findOne({ + const page: Page | null = await pageRepo.findOne({ pageId, domain: ctx.subdomain._id, }); @@ -462,7 +472,7 @@ export const createPage = async ({ ); try { - const page: Page = await PageModel.create({ + const page: Page = await pageRepo.create({ domain: ctx.subdomain._id, pageId: uniquePageId, type: site, @@ -546,7 +556,7 @@ export const deleteBlock = async ({ if (!checkPermission(ctx.user.permissions, [permissions.manageSite])) { throw new Error(responses.action_not_allowed); } - const page: Page | null = await PageModel.findOne({ + const page: Page | null = await pageRepo.findOne({ pageId, domain: ctx.subdomain._id, }); diff --git a/apps/web/graphql/paymentplans/logic.ts b/apps/web/graphql/paymentplans/logic.ts index 784a77c8d..cb1255b5a 100644 --- a/apps/web/graphql/paymentplans/logic.ts +++ b/apps/web/graphql/paymentplans/logic.ts @@ -12,7 +12,13 @@ import constants from "@config/constants"; import { checkPermission } from "@courselit/utils"; import PaymentPlanModel, { InternalPaymentPlan } from "@models/PaymentPlan"; import { Domain } from "@models/Domain"; -import { InternalCourse } from "@courselit/orm-models"; +import { + InternalCourse, + MembershipRepository, + CourseRepository, + PaymentPlanRepository, + CommunityRepository, +} from "@courselit/orm-models"; import GQLContext from "@models/GQLContext"; import { checkDuplicatePlan, @@ -23,6 +29,12 @@ import mongoose from "mongoose"; import MembershipModel from "@models/Membership"; import { runPostMembershipTasks } from "../users/logic"; import ActivityModel from "@models/Activity"; + +const communityRepo = new CommunityRepository(CommunityModel); +const courseRepo = new CourseRepository(CourseModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); + const { MembershipEntityType: membershipEntityType } = Constants; const { permissions } = constants; @@ -32,12 +44,12 @@ async function fetchEntity( ctx: any, ): Promise { if (entityType === membershipEntityType.COURSE) { - return (await CourseModel.findOne({ + return (await courseRepo.findOne({ domain: ctx.subdomain._id, courseId: entityId, })) as InternalCourse; } else if (entityType === membershipEntityType.COMMUNITY) { - return (await CommunityModel.findOne({ + return (await communityRepo.findOne({ domain: ctx.subdomain._id, communityId: entityId, deleted: false, @@ -73,7 +85,7 @@ function checkEntityManagementPermission( export async function getPlan({ planId, ctx }: { planId: string; ctx: any }) { checkIfAuthenticated(ctx); - const plan = await PaymentPlanModel.findOne({ + const plan = await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, planId, archived: false, @@ -196,7 +208,7 @@ export async function createPlan({ await checkDuplicatePlan(paymentPlanPayload); await checkIncludedProducts(ctx.subdomain._id, paymentPlanPayload); - const paymentPlan = await PaymentPlanModel.create(paymentPlanPayload); + const paymentPlan = await paymentPlanRepo.create(paymentPlanPayload); if (!entity.defaultPaymentPlan) { (entity as InternalCourse | InternalCommunity).defaultPaymentPlan = @@ -234,7 +246,7 @@ export async function updatePlan({ }): Promise { checkIfAuthenticated(ctx); - const paymentPlan = await PaymentPlanModel.findOne({ + const paymentPlan = await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, planId, archived: false, @@ -288,7 +300,7 @@ export async function archivePaymentPlan({ }): Promise { checkIfAuthenticated(ctx); - const paymentPlan = await PaymentPlanModel.findOne({ + const paymentPlan = await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, planId, archived: false, @@ -344,7 +356,7 @@ export async function changeDefaultPlan({ checkEntityManagementPermission(entityType, ctx); - const paymentPlan = await PaymentPlanModel.findOne({ + const paymentPlan = await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, planId, archived: false, @@ -362,7 +374,7 @@ export async function changeDefaultPlan({ } export async function getInternalPaymentPlan(ctx: any) { - return await PaymentPlanModel.findOne({ + return await paymentPlanRepo.findOne({ domain: ctx.subdomain._id, internal: true, }); @@ -372,7 +384,7 @@ export async function createInternalPaymentPlan( domain: Domain, userId: string, ) { - return await PaymentPlanModel.create({ + return await paymentPlanRepo.create({ domain: domain._id, name: constants.internalPaymentPlanName, type: Constants.PaymentPlanType.FREE, @@ -428,14 +440,14 @@ export async function addIncludedProductsMemberships({ paymentPlan: PaymentPlan; sessionId: string; }) { - const courses = await CourseModel.find({ + const courses = await courseRepo.find({ domain, courseId: { $in: paymentPlan.includedProducts }, published: true, }); for (const course of courses) { - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain, userId, entityId: course.courseId, diff --git a/apps/web/graphql/product-discussions/helpers.ts b/apps/web/graphql/product-discussions/helpers.ts index b83548ce6..82b1974f3 100644 --- a/apps/web/graphql/product-discussions/helpers.ts +++ b/apps/web/graphql/product-discussions/helpers.ts @@ -19,6 +19,10 @@ import { checkOwnershipWithoutModel, } from "@/lib/graphql"; import { getMembershipStatus } from "../users/logic"; +import { LessonRepository, CourseRepository } from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const lessonRepo = new LessonRepository(LessonModel); const { permissions } = appConstants; @@ -84,7 +88,7 @@ export async function validateDiscussionTargetForLearner({ throw new Error(responses.action_not_allowed); } - const product = await CourseModel.findOne({ + const product = await courseRepo.findOne({ domain: ctx.subdomain._id, courseId: productId, }); @@ -115,7 +119,7 @@ export async function validateDiscussionTargetForLearner({ let lesson; if (isAdminOrCreator) { - lesson = await LessonModel.findOne({ + lesson = await lessonRepo.findOne({ lessonId: entityId, domain: ctx.subdomain._id, courseId: productId, diff --git a/apps/web/graphql/product-discussions/logic.ts b/apps/web/graphql/product-discussions/logic.ts index f11543721..2ee544203 100644 --- a/apps/web/graphql/product-discussions/logic.ts +++ b/apps/web/graphql/product-discussions/logic.ts @@ -35,6 +35,9 @@ import { validateDiscussionTargetForLearner, getNextReportStatus, } from "./helpers"; +import { CourseRepository } from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); export const COURSE_DISCUSSION_RATE_LIMITS = { commentsPerMinute: { window: 60 * 1000, limit: 5 }, @@ -1629,7 +1632,7 @@ async function validateProductDiscussionAdmin({ }) { checkIfAuthenticated(ctx); - const product = await CourseModel.findOne({ + const product = await courseRepo.findOne({ domain: ctx.subdomain._id, courseId: productId, }); @@ -1661,7 +1664,7 @@ async function getAccessibleDiscussionLessonIds({ productId: string; preview?: boolean; }) { - const product = await CourseModel.findOne({ + const product = await courseRepo.findOne({ domain: ctx.subdomain._id, courseId: productId, type: Constants.CourseType.COURSE, diff --git a/apps/web/graphql/product-discussions/types.ts b/apps/web/graphql/product-discussions/types.ts index 5684eac81..22601ba9c 100644 --- a/apps/web/graphql/product-discussions/types.ts +++ b/apps/web/graphql/product-discussions/types.ts @@ -16,6 +16,11 @@ import UserModel from "@/models/User"; import userTypes from "../users/types"; import { extractTextFromTextEditorContent } from "@courselit/utils"; import { Constants } from "@courselit/common-models"; +import { UserRepository, LessonRepository } from "@courselit/orm-models"; + +const lessonRepo = new LessonRepository(LessonModel); +const userRepo = new UserRepository(UserModel); + const { ProductDiscussionEntityType, ProductDiscussionContentType, @@ -105,7 +110,7 @@ const productDiscussionReply = new GraphQLObjectType({ user: { type: userTypes.userType, resolve: async (reply, _, ctx) => { - return await UserModel.findOne({ + return await userRepo.findOne({ domain: ctx.subdomain._id, userId: reply.userId, }); @@ -196,7 +201,7 @@ const productDiscussionComment = new GraphQLObjectType({ user: { type: userTypes.userType, resolve: async (comment, _, ctx) => { - return await UserModel.findOne({ + return await userRepo.findOne({ domain: ctx.subdomain._id, userId: comment.userId, }); @@ -295,7 +300,7 @@ const productDiscussionReport = new GraphQLObjectType({ resolve: async (report, _, ctx) => { if (report.entityType !== ProductDiscussionEntityType.LESSON) return null; - const lesson = await LessonModel.findOne({ + const lesson = await lessonRepo.findOne({ domain: ctx.subdomain._id, lessonId: report.entityId, }); @@ -333,7 +338,7 @@ const productDiscussionReport = new GraphQLObjectType({ replyId: report.contentId, }); if (!item) return null; - const user = await UserModel.findOne({ + const user = await userRepo.findOne({ domain: ctx.subdomain._id, userId: item.userId, }); @@ -343,7 +348,7 @@ const productDiscussionReport = new GraphQLObjectType({ reporterName: { type: GraphQLString, resolve: async (report, _, ctx) => { - const user = await UserModel.findOne({ + const user = await userRepo.findOne({ domain: ctx.subdomain._id, userId: report.userId, }); diff --git a/apps/web/graphql/settings/__tests__/payment.test.ts b/apps/web/graphql/settings/__tests__/payment.test.ts index a432887cd..d11c23998 100644 --- a/apps/web/graphql/settings/__tests__/payment.test.ts +++ b/apps/web/graphql/settings/__tests__/payment.test.ts @@ -4,6 +4,10 @@ import { responses } from "@/config/strings"; import DomainModel from "@/models/Domain"; import UserModel from "@/models/User"; import { resetPaymentMethod } from "../logic"; +import { DomainRepository, UserRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); +const userRepo = new UserRepository(UserModel); const SUITE_PREFIX = `payment-settings-${Date.now()}`; const id = (suffix: string) => `${SUITE_PREFIX}-${suffix}`; @@ -16,7 +20,7 @@ describe("Payment settings", () => { let mockCtx: any; beforeAll(async () => { - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), settings: { @@ -26,7 +30,7 @@ describe("Payment settings", () => { }, }); - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin"), email: email("admin"), @@ -37,7 +41,7 @@ describe("Payment settings", () => { purchases: [], }); - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: id("regular"), email: email("regular"), @@ -94,7 +98,7 @@ describe("Payment settings", () => { UIConstants.PAYMENT_METHOD_NONE, ); - const updatedDomain = await DomainModel.findById(testDomain._id); + const updatedDomain = await domainRepo.findById(testDomain._id); expect(updatedDomain?.settings?.paymentMethod).toBe( UIConstants.PAYMENT_METHOD_NONE, ); diff --git a/apps/web/graphql/settings/__tests__/sso.test.ts b/apps/web/graphql/settings/__tests__/sso.test.ts index 30e60084d..c134e578e 100644 --- a/apps/web/graphql/settings/__tests__/sso.test.ts +++ b/apps/web/graphql/settings/__tests__/sso.test.ts @@ -23,6 +23,10 @@ import { LOGIN_PROVIDER_SSO_BUTTON, LOGIN_PROVIDER_SSO_LABEL, } from "../../../ui-config/strings"; +import { DomainRepository, UserRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); +const userRepo = new UserRepository(UserModel); jest.mock("@/lib/domain-cache", () => ({ invalidateDomainCache: jest.fn(), @@ -46,7 +50,7 @@ describe("SSO Logic Tests", () => { beforeAll(async () => { // Create test domain with SSO feature enabled - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: id("domain"), email: email("domain"), features: [Constants.Features.SSO], @@ -56,7 +60,7 @@ describe("SSO Logic Tests", () => { }); // Create admin user with manageSettings permission - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: id("admin"), email: email("admin"), @@ -68,7 +72,7 @@ describe("SSO Logic Tests", () => { }); // Create regular user without permissions - regularUser = await UserModel.create({ + regularUser = await userRepo.create({ domain: testDomain._id, userId: id("regular"), email: email("regular"), @@ -99,7 +103,7 @@ describe("SSO Logic Tests", () => { }, ); // Refresh local domain object - const updatedDomain = await DomainModel.findById(testDomain._id); + const updatedDomain = await domainRepo.findById(testDomain._id); mockCtx.subdomain = updatedDomain; }); @@ -177,7 +181,7 @@ describe("SSO Logic Tests", () => { ); // Check if domain settings updated (refresh context first or check DB) - const domain = await DomainModel.findById(testDomain._id); + const domain = await domainRepo.findById(testDomain._id); expect(domain!.settings.ssoTrustedDomain).toBe( new URL(validConfig.entryPoint).origin, ); @@ -355,7 +359,7 @@ describe("SSO Logic Tests", () => { expect(provider).toBeNull(); // Verify login disabled - const domain = await DomainModel.findById(testDomain._id); + const domain = await domainRepo.findById(testDomain._id); expect(domain!.settings.logins).not.toContain( Constants.LoginProvider.SSO, ); @@ -394,7 +398,7 @@ describe("SSO Logic Tests", () => { }); expect(provider).toBeNull(); - const domain = await DomainModel.findById(testDomain._id); + const domain = await domainRepo.findById(testDomain._id); expect(domain!.settings.logins).not.toContain( Constants.LoginProvider.GOOGLE, ); diff --git a/apps/web/graphql/settings/logic.ts b/apps/web/graphql/settings/logic.ts index aa39d58e0..f85e43bb0 100644 --- a/apps/web/graphql/settings/logic.ts +++ b/apps/web/graphql/settings/logic.ts @@ -26,6 +26,9 @@ import { RuntimeLoginProvider, } from "@/lib/login-providers"; import { invalidateDomainCache } from "@/lib/domain-cache"; +import { DomainRepository } from "@courselit/orm-models"; + +const domainRepo = new DomainRepository(DomainModel); const { permissions } = constants; @@ -149,7 +152,7 @@ export const updateSiteInfo = async ( throw new Error(responses.action_not_allowed); } - const domain: Domain | null = await DomainModel.findById(ctx.subdomain._id); + const domain: Domain | null = await domainRepo.findById(ctx.subdomain._id); if (!domain) { return null; } @@ -199,7 +202,7 @@ export const updateDraftTypefaces = async ( throw new Error(responses.action_not_allowed); } - const domain: Domain | null = await DomainModel.findById(ctx.subdomain._id); + const domain: Domain | null = await domainRepo.findById(ctx.subdomain._id); if (!domain) { return null; } @@ -221,7 +224,7 @@ export const updatePaymentInfo = async ( throw new Error(responses.action_not_allowed); } - const domain: Domain | null = await DomainModel.findById(ctx.subdomain._id); + const domain: Domain | null = await domainRepo.findById(ctx.subdomain._id); if (!domain) { return null; } @@ -264,7 +267,7 @@ export const resetPaymentMethod = async (ctx: GQLContext) => { throw new Error(responses.action_not_allowed); } - const domain: Domain | null = await DomainModel.findById(ctx.subdomain._id); + const domain: Domain | null = await domainRepo.findById(ctx.subdomain._id); if (!domain) { return null; } @@ -642,7 +645,7 @@ export const removeGoogleProvider = async (ctx: GQLContext) => { }; export const getFeatures = async (ctx: GQLContext) => { - await DomainModel.findOne({ + await domainRepo.findOne({ _id: ctx.subdomain._id, }); diff --git a/apps/web/graphql/users/__tests__/delete-user.test.ts b/apps/web/graphql/users/__tests__/delete-user.test.ts index 274892fab..0030a5e73 100644 --- a/apps/web/graphql/users/__tests__/delete-user.test.ts +++ b/apps/web/graphql/users/__tests__/delete-user.test.ts @@ -40,6 +40,33 @@ import constants from "@/config/constants"; import { Constants } from "@courselit/common-models"; import { deleteMedia } from "@/services/medialit"; import { deleteCommunityPosts } from "../../communities/logic"; +import { + MembershipRepository, + UserRepository, + NotificationRepository, + CourseRepository, + CommunityRepository, + CommunityCommentRepository, + CommunityPostRepository, + LessonRepository, + PaymentPlanRepository, + PageRepository, + DomainRepository, +} from "@courselit/orm-models"; + +const communityCommentRepo = new CommunityCommentRepository( + CommunityCommentModel, +); +const communityPostRepo = new CommunityPostRepository(CommunityPostModel); +const communityRepo = new CommunityRepository(CommunityModel); +const courseRepo = new CourseRepository(CourseModel); +const domainRepo = new DomainRepository(DomainModel); +const lessonRepo = new LessonRepository(LessonModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const notificationRepo = new NotificationRepository(NotificationModel); +const pageRepo = new PageRepository(PageModel); +const paymentPlanRepo = new PaymentPlanRepository(PaymentPlanModel); +const userRepo = new UserRepository(UserModel); // Mock external dependencies jest.mock("@/services/medialit", () => ({ @@ -73,7 +100,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { beforeAll(async () => { // Create test domain with unique name to avoid conflicts with other tests - testDomain = await DomainModel.create({ + testDomain = await domainRepo.create({ name: duId("domain"), email: duEmail("domain"), tags: ["tag1", "tag2"], @@ -82,7 +109,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { beforeEach(async () => { // Create admin user (deleter) - adminUser = await UserModel.create({ + adminUser = await userRepo.create({ domain: testDomain._id, userId: duId("admin-user"), name: "Admin User", @@ -98,7 +125,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Create target user (to be deleted) - targetUser = await UserModel.create({ + targetUser = await userRepo.create({ domain: testDomain._id, userId: duId("target-user"), name: "Target User", @@ -188,7 +215,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should require manageUsers permission", async () => { - const unauthorizedUser = await UserModel.create({ + const unauthorizedUser = await userRepo.create({ domain: testDomain._id, userId: duId("unauth-user"), email: duEmail("unauth"), @@ -217,7 +244,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should prevent deleting the domain owner used as the API actor", async () => { - const domainOwner = await UserModel.create({ + const domainOwner = await userRepo.create({ domain: testDomain._id, userId: duId("domain-owner"), name: "Domain Owner", @@ -233,7 +260,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { ).rejects.toThrow(responses.action_not_allowed); await expect( - UserModel.findOne({ + userRepo.findOne({ domain: testDomain._id, userId: domainOwner.userId, }), @@ -269,7 +296,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { describe("Business Entity Migration", () => { it("should migrate course ownership to deleter", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "du-course-mig-123", title: "Test Course", @@ -285,14 +312,14 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const updatedCourse = await CourseModel.findOne({ + const updatedCourse = await courseRepo.findOne({ courseId: course.courseId, }); expect(updatedCourse?.creatorId).toBe(adminUser.userId); }); it("should migrate course pages to deleter", async () => { - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "du-course-123", title: "Test Course", @@ -306,7 +333,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { pageId: "du-page-123", }); - const page = await PageModel.create({ + const page = await pageRepo.create({ domain: testDomain._id, pageId: "du-page-123", type: constants.product, @@ -318,7 +345,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const updatedPage = await PageModel.findOne({ + const updatedPage = await pageRepo.findOne({ pageId: page.pageId, }); expect(updatedPage?.creatorId).toBe(adminUser.userId); @@ -418,7 +445,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should migrate payment plans to deleter", async () => { - const plan = await PaymentPlanModel.create({ + const plan = await paymentPlanRepo.create({ domain: testDomain._id, planId: "plan-123", userId: targetUser.userId, @@ -433,14 +460,14 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const updatedPlan = await PaymentPlanModel.findOne({ + const updatedPlan = await paymentPlanRepo.findOne({ planId: plan.planId, }); expect(updatedPlan?.userId).toBe(adminUser.userId); }); it("should migrate lessons to deleter", async () => { - const lesson = await LessonModel.create({ + const lesson = await lessonRepo.create({ domain: testDomain._id, lessonId: "lesson-123", title: "Test Lesson", @@ -455,14 +482,14 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const updatedLesson = await LessonModel.findOne({ + const updatedLesson = await lessonRepo.findOne({ lessonId: lesson.lessonId, }); expect(updatedLesson?.creatorId).toBe(adminUser.userId); }); it("should transfer community moderator role to deleter", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "comm-123", name: "Test Community", @@ -470,7 +497,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { slug: "du-page-comm-123", }); - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-123", userId: targetUser.userId, @@ -485,7 +512,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const newModeratorMembership = await MembershipModel.findOne({ + const newModeratorMembership = await membershipRepo.findOne({ userId: adminUser.userId, entityId: community.communityId, entityType: Constants.MembershipEntityType.COMMUNITY, @@ -501,7 +528,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should upgrade existing membership when transferring moderator role", async () => { - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "comm-123", name: "Test Community", @@ -510,7 +537,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Target user is moderator - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-target", userId: targetUser.userId, @@ -524,7 +551,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Admin already has regular membership - const existingMembership = await MembershipModel.create({ + const existingMembership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-admin", userId: adminUser.userId, @@ -538,7 +565,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const updatedMembership = await MembershipModel.findOne({ + const updatedMembership = await membershipRepo.findOne({ membershipId: existingMembership.membershipId, }); @@ -550,7 +577,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { ); // Target's membership should be deleted - const targetMembership = await MembershipModel.findOne({ + const targetMembership = await membershipRepo.findOne({ membershipId: "mem-target", }); expect(targetMembership).toBeNull(); @@ -563,7 +590,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { describe("Personal Data Cleanup", () => { it("should delete user's notifications (received)", async () => { - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: "notif-1", userId: DU_OTHER_USER_ID, @@ -574,14 +601,14 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const notifications = await NotificationModel.find({ + const notifications = await notificationRepo.find({ forUserId: targetUser.userId, }); expect(notifications).toHaveLength(0); }); it("should delete user's notifications (created)", async () => { - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: "notif-2", userId: targetUser.userId, @@ -592,7 +619,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const notifications = await NotificationModel.find({ + const notifications = await notificationRepo.find({ userId: targetUser.userId, }); expect(notifications).toHaveLength(0); @@ -864,7 +891,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should remove user from post likes arrays", async () => { - await CommunityPostModel.create({ + await communityPostRepo.create({ domain: testDomain._id, postId: "post-123", userId: DU_OTHER_USER_ID, @@ -876,7 +903,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const post = await CommunityPostModel.findOne({ + const post = await communityPostRepo.findOne({ postId: "post-123", }); expect(post?.likes).not.toContain(targetUser.userId); @@ -884,7 +911,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should remove user from comment likes arrays", async () => { - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, commentId: "comment-123", postId: "post-123", @@ -897,7 +924,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ commentId: "comment-123", }); expect(comment?.likes).not.toContain(targetUser.userId); @@ -905,7 +932,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should remove user from reply likes arrays", async () => { - await CommunityCommentModel.create({ + await communityCommentRepo.create({ domain: testDomain._id, commentId: "comment-123", postId: "post-123", @@ -926,7 +953,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const comment = await CommunityCommentModel.findOne({ + const comment = await communityCommentRepo.findOne({ commentId: "comment-123", }); const reply = comment?.replies[0]; @@ -935,7 +962,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should delete memberships and associated invoices", async () => { - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-123", userId: targetUser.userId, @@ -959,7 +986,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ userId: targetUser.userId, }); expect(memberships).toHaveLength(0); @@ -977,7 +1004,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { cancel: mockCancel, }); - const membership = await MembershipModel.create({ + const membership = await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-sub-123", userId: targetUser.userId, @@ -1007,7 +1034,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { expect(mockCancel).toHaveBeenCalledWith("sub_stripe_123"); // Verify membership was deleted - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ userId: targetUser.userId, membershipId: membership.membershipId, }); @@ -1029,7 +1056,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { it("should delete the user document", async () => { await deleteUser(targetUser.userId, mockCtx); - const user = await UserModel.findOne({ userId: targetUser.userId }); + const user = await userRepo.findOne({ userId: targetUser.userId }); expect(user).toBeNull(); }); }); @@ -1061,7 +1088,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); it("should remove user from course customers", async () => { - await CourseModel.create({ + await courseRepo.create({ domain: testDomain._id, courseId: "du-course-123", title: "Test Course", @@ -1077,7 +1104,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); - const course = await CourseModel.findOne({ + const course = await courseRepo.findOne({ courseId: "du-course-123", }); expect(course?.customers).not.toContain(targetUser.userId); @@ -1092,7 +1119,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { describe("Integration Tests", () => { it("should handle complex scenario with multiple entities", async () => { // Create course - const course = await CourseModel.create({ + const course = await courseRepo.create({ domain: testDomain._id, courseId: "du-course-123", title: "Test Course", @@ -1107,7 +1134,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Create community - const community = await CommunityModel.create({ + const community = await communityRepo.create({ domain: testDomain._id, communityId: "comm-123", name: "Test Community", @@ -1116,7 +1143,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Create membership - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-123", userId: targetUser.userId, @@ -1138,7 +1165,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); // Create notifications - await NotificationModel.create({ + await notificationRepo.create({ domain: testDomain._id, notificationId: "notif-1", userId: targetUser.userId, @@ -1150,14 +1177,14 @@ describe("deleteUser - Comprehensive Test Suite", () => { await deleteUser(targetUser.userId, mockCtx); // Verify course migrated - const updatedCourse = await CourseModel.findOne({ + const updatedCourse = await courseRepo.findOne({ courseId: course.courseId, }); expect(updatedCourse?.creatorId).toBe(adminUser.userId); expect(updatedCourse?.customers).not.toContain(targetUser.userId); // Verify community moderator migrated - const moderatorMembership = await MembershipModel.findOne({ + const moderatorMembership = await membershipRepo.findOne({ userId: adminUser.userId, entityId: community.communityId, }); @@ -1171,13 +1198,13 @@ describe("deleteUser - Comprehensive Test Suite", () => { }); expect(activities).toHaveLength(0); - const notifications = await NotificationModel.find({ + const notifications = await notificationRepo.find({ userId: targetUser.userId, }); expect(notifications).toHaveLength(0); // Verify user deleted - const user = await UserModel.findOne({ userId: targetUser.userId }); + const user = await userRepo.findOne({ userId: targetUser.userId }); expect(user).toBeNull(); // Verify avatar deleted @@ -1188,12 +1215,12 @@ describe("deleteUser - Comprehensive Test Suite", () => { const result = await deleteUser(targetUser.userId, mockCtx); expect(result).toBe(true); - const user = await UserModel.findOne({ userId: targetUser.userId }); + const user = await userRepo.findOne({ userId: targetUser.userId }); expect(user).toBeNull(); }); it("should handle user with subscription cancellation", async () => { - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: "mem-123", userId: targetUser.userId, @@ -1211,7 +1238,7 @@ describe("deleteUser - Comprehensive Test Suite", () => { const { getPaymentMethodFromSettings } = require("@/payments-new"); expect(getPaymentMethodFromSettings).toHaveBeenCalled(); - const memberships = await MembershipModel.find({ + const memberships = await membershipRepo.find({ userId: targetUser.userId, }); expect(memberships).toHaveLength(0); diff --git a/apps/web/graphql/users/__tests__/logic.test.ts b/apps/web/graphql/users/__tests__/logic.test.ts index d3e96dec1..08d17416c 100644 --- a/apps/web/graphql/users/__tests__/logic.test.ts +++ b/apps/web/graphql/users/__tests__/logic.test.ts @@ -35,6 +35,15 @@ import { Constants, UIConstants } from "@courselit/common-models"; import { seedNotificationPreferencesForUser } from "../../notifications/logic"; import { recordActivity } from "@/lib/record-activity"; import { triggerSequences } from "@/lib/trigger-sequences"; +import { + MembershipRepository, + CourseRepository, + UserRepository, +} from "@courselit/orm-models"; + +const courseRepo = new CourseRepository(CourseModel); +const membershipRepo = new MembershipRepository(MembershipModel); +const userRepo = new UserRepository(UserModel); const seedNotificationPreferencesForUserMock = seedNotificationPreferencesForUser as jest.Mock; @@ -81,7 +90,7 @@ describe("updateUser", () => { }); it("prevents changing permissions for the school owner", async () => { - await UserModel.create({ + await userRepo.create({ userId: "owner", email: ownerEmail, domain: domainId, @@ -108,7 +117,7 @@ describe("updateUser", () => { }); it("continues to allow non-permission updates for the school owner", async () => { - await UserModel.create({ + await userRepo.create({ userId: "owner", email: ownerEmail, name: "Old Owner", @@ -136,7 +145,7 @@ describe("updateUser", () => { }); it("prevents deactivating the school owner", async () => { - await UserModel.create({ + await userRepo.create({ userId: "owner", email: ownerEmail, domain: domainId, @@ -336,7 +345,7 @@ describe("Certificate generation", () => { it("should generate demo certificate when courseId is provided", async () => { // Create a test course - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -370,7 +379,7 @@ describe("Certificate generation", () => { it("should generate certificate with complete data", async () => { // Create test data - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -386,7 +395,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-123", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -394,7 +403,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Advanced Course", creatorId: "creator-123", @@ -470,7 +479,7 @@ describe("Certificate generation", () => { it("should throw error when course is not found", async () => { // Create a certificate but no corresponding course - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -493,7 +502,7 @@ describe("Certificate generation", () => { it("should use fallback values when template is missing", async () => { // Create test data without template - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -502,7 +511,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -510,7 +519,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -552,7 +561,7 @@ describe("Certificate generation", () => { it("should use fallback values when template has partial data", async () => { // Create test data with partial template - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -561,7 +570,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -569,7 +578,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -624,7 +633,7 @@ describe("Certificate generation", () => { it("should use user email as fallback when user name is missing", async () => { // Create test data with user having no name - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: null, email: "john@example.com", @@ -633,7 +642,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -641,7 +650,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -669,7 +678,7 @@ describe("Certificate generation", () => { it("should use creator name as fallback when template signatureName is missing", async () => { // Create test data with template missing signatureName - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -678,7 +687,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -686,7 +695,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -724,7 +733,7 @@ describe("Certificate generation", () => { it("should use domain logo as fallback when template logo is missing", async () => { // Create test data with template missing logo - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -733,7 +742,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -741,7 +750,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -798,7 +807,7 @@ describe("Certificate generation", () => { } as any; // Create test data - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -807,7 +816,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -815,7 +824,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -858,7 +867,7 @@ describe("Certificate generation", () => { it("should handle missing creator gracefully", async () => { // Create test data with missing creator const uniqueId = Date.now(); - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -867,7 +876,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "nonexistent-creator", @@ -899,7 +908,7 @@ describe("Certificate generation", () => { it("should handle missing course pageId gracefully", async () => { // Create test data with course missing pageId const uniqueId = Date.now(); - await UserModel.create({ + await userRepo.create({ userId: "user-123", name: "John Doe", email: "john@example.com", @@ -908,7 +917,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-user", }); - await UserModel.create({ + await userRepo.create({ userId: "creator-123", name: "Jane Smith", email: "jane@example.com", @@ -916,7 +925,7 @@ describe("Certificate generation", () => { unsubscribeToken: "unsubscribe-token-creator", }); - await CourseModel.create({ + await courseRepo.create({ courseId: "course-123", title: "Test Course", creatorId: "creator-123", @@ -958,7 +967,7 @@ describe("findMembership", () => { email: `${fId("owner")}@example.com`, }); - testUser = await UserModel.create({ + testUser = await userRepo.create({ userId: fId("user"), email: `${fId("user")}@example.com`, name: "Test User", @@ -969,7 +978,7 @@ describe("findMembership", () => { unsubscribeToken: fId("unsubscribe"), }); - testCourse = await CourseModel.create({ + testCourse = await courseRepo.create({ domain: testDomain._id, courseId: fId("course"), title: "Test Course", @@ -992,7 +1001,7 @@ describe("findMembership", () => { }); it("returns the membership when it exists", async () => { - await MembershipModel.create({ + await membershipRepo.create({ domain: testDomain._id, membershipId: fId("membership"), sessionId: fId("session"), diff --git a/apps/web/graphql/users/helpers.ts b/apps/web/graphql/users/helpers.ts index d693b0671..dfc805b6a 100644 --- a/apps/web/graphql/users/helpers.ts +++ b/apps/web/graphql/users/helpers.ts @@ -217,11 +217,11 @@ export async function migrateBusinessEntities( communityIds.add(membership.entityId); const existingMembership = await membershipRepo.findOne({ - domain: ctx.subdomain._id, - userId: deleterUser.userId, - entityId: membership.entityId, - entityType: Constants.MembershipEntityType.COMMUNITY, - }); + domain: ctx.subdomain._id, + userId: deleterUser.userId, + entityId: membership.entityId, + entityType: Constants.MembershipEntityType.COMMUNITY, + }); if (existingMembership) { // Update existing membership to moderator role diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts index 8346fd512..002423063 100644 --- a/apps/web/graphql/users/logic.ts +++ b/apps/web/graphql/users/logic.ts @@ -868,11 +868,11 @@ export const getMembership = async ({ planId: string; }): Promise => { const existingMembership = await membershipRepo.findOne({ - domain: domainId, - userId, - entityType, - entityId, - }); + domain: domainId, + userId, + entityType, + entityId, + }); let membership: InternalMembership = existingMembership ||