@headless-lms/adapter-db 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +79 -0
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +7 -5
  5. package/dist/index.js +8 -6
  6. package/dist/migrate.js.map +1 -1
  7. package/dist/repositories/admin.d.ts +23 -0
  8. package/dist/repositories/admin.js +393 -0
  9. package/dist/repositories/admin.js.map +1 -0
  10. package/dist/repositories/assets.d.ts +6 -2
  11. package/dist/repositories/assets.js +53 -9
  12. package/dist/repositories/assets.js.map +1 -1
  13. package/dist/repositories/automations.d.ts +12 -6
  14. package/dist/repositories/automations.js +79 -11
  15. package/dist/repositories/automations.js.map +1 -1
  16. package/dist/repositories/chats.d.ts +55 -0
  17. package/dist/repositories/chats.js +454 -0
  18. package/dist/repositories/chats.js.map +1 -0
  19. package/dist/repositories/content.d.ts +44 -7
  20. package/dist/repositories/content.js +458 -24
  21. package/dist/repositories/content.js.map +1 -1
  22. package/dist/repositories/credentials.js +2 -0
  23. package/dist/repositories/credentials.js.map +1 -1
  24. package/dist/repositories/discussion.d.ts +11 -4
  25. package/dist/repositories/discussion.js +72 -15
  26. package/dist/repositories/discussion.js.map +1 -1
  27. package/dist/repositories/entitlements.d.ts +36 -10
  28. package/dist/repositories/entitlements.js +238 -23
  29. package/dist/repositories/entitlements.js.map +1 -1
  30. package/dist/repositories/grant-covers.d.ts +20 -0
  31. package/dist/repositories/grant-covers.js +55 -0
  32. package/dist/repositories/grant-covers.js.map +1 -0
  33. package/dist/repositories/identity.d.ts +4 -2
  34. package/dist/repositories/identity.js +34 -1
  35. package/dist/repositories/identity.js.map +1 -1
  36. package/dist/repositories/integrations.d.ts +1 -1
  37. package/dist/repositories/integrations.js +2 -0
  38. package/dist/repositories/integrations.js.map +1 -1
  39. package/dist/repositories/member.d.ts +16 -0
  40. package/dist/repositories/member.js +63 -0
  41. package/dist/repositories/member.js.map +1 -0
  42. package/dist/repositories/members.js +4 -2
  43. package/dist/repositories/members.js.map +1 -1
  44. package/dist/repositories/notifications.d.ts +23 -0
  45. package/dist/repositories/notifications.js +70 -0
  46. package/dist/repositories/notifications.js.map +1 -0
  47. package/dist/repositories/org-user-profile.d.ts +214 -1
  48. package/dist/repositories/org-user-profile.js +23 -2
  49. package/dist/repositories/org-user-profile.js.map +1 -1
  50. package/dist/repositories/organizations.d.ts +6 -7
  51. package/dist/repositories/organizations.js +45 -52
  52. package/dist/repositories/organizations.js.map +1 -1
  53. package/dist/repositories/outbox.js +3 -0
  54. package/dist/repositories/outbox.js.map +1 -1
  55. package/dist/repositories/pg-errors.d.ts +53 -5
  56. package/dist/repositories/pg-errors.js +99 -8
  57. package/dist/repositories/pg-errors.js.map +1 -1
  58. package/dist/repositories/posts.d.ts +39 -0
  59. package/dist/repositories/posts.js +255 -0
  60. package/dist/repositories/posts.js.map +1 -0
  61. package/dist/repositories/progress.d.ts +1 -1
  62. package/dist/repositories/progress.js +3 -0
  63. package/dist/repositories/progress.js.map +1 -1
  64. package/dist/repositories/settings.js +2 -0
  65. package/dist/repositories/settings.js.map +1 -1
  66. package/dist/schema/assets.d.ts +36 -2
  67. package/dist/schema/assets.js +12 -5
  68. package/dist/schema/assets.js.map +1 -1
  69. package/dist/schema/automations.d.ts +123 -8
  70. package/dist/schema/automations.js +11 -4
  71. package/dist/schema/automations.js.map +1 -1
  72. package/dist/schema/better-auth.d.ts +1613 -238
  73. package/dist/schema/better-auth.js +107 -22
  74. package/dist/schema/better-auth.js.map +1 -1
  75. package/dist/schema/content.d.ts +909 -22
  76. package/dist/schema/content.js +157 -13
  77. package/dist/schema/content.js.map +1 -1
  78. package/dist/schema/credentials.js +1 -1
  79. package/dist/schema/credentials.js.map +1 -1
  80. package/dist/schema/discussion.d.ts +2079 -55
  81. package/dist/schema/discussion.js +430 -13
  82. package/dist/schema/discussion.js.map +1 -1
  83. package/dist/schema/entitlements.d.ts +484 -2
  84. package/dist/schema/entitlements.js +121 -6
  85. package/dist/schema/entitlements.js.map +1 -1
  86. package/dist/schema/identity.d.ts +517 -1
  87. package/dist/schema/identity.js +118 -2
  88. package/dist/schema/identity.js.map +1 -1
  89. package/dist/schema/index.d.ts +6 -6
  90. package/dist/schema/index.js +32 -7
  91. package/dist/schema/index.js.map +1 -1
  92. package/dist/schema/integrations.js +1 -1
  93. package/dist/schema/integrations.js.map +1 -1
  94. package/dist/schema/organizations.d.ts +21 -4
  95. package/dist/schema/organizations.js +4 -1
  96. package/dist/schema/organizations.js.map +1 -1
  97. package/dist/schema/outbox.d.ts +17 -0
  98. package/dist/schema/outbox.js +1 -0
  99. package/dist/schema/outbox.js.map +1 -1
  100. package/dist/schema/progress.d.ts +17 -0
  101. package/dist/schema/progress.js +1 -0
  102. package/dist/schema/progress.js.map +1 -1
  103. package/dist/schema/settings.js.map +1 -1
  104. package/dist/unit-of-work.js +7 -2
  105. package/dist/unit-of-work.js.map +1 -1
  106. package/drizzle/0000_baseline.sql +0 -1
  107. package/drizzle/0001_new_sentry.sql +43 -0
  108. package/drizzle/0002_automation_kinds.sql +7 -0
  109. package/drizzle/0003_community.sql +340 -0
  110. package/drizzle/0004_assets_sources.sql +1 -0
  111. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  112. package/drizzle/0006_community_review_fixes.sql +24 -0
  113. package/drizzle/0007_notifications.sql +37 -0
  114. package/drizzle/0008_content_access.sql +13 -0
  115. package/drizzle/0009_community_indexes.sql +56 -0
  116. package/drizzle/0010_mcp_access.sql +114 -0
  117. package/drizzle/meta/0000_snapshot.json +167 -597
  118. package/drizzle/meta/0001_snapshot.json +3769 -0
  119. package/drizzle/meta/0002_snapshot.json +3770 -0
  120. package/drizzle/meta/0003_snapshot.json +7061 -0
  121. package/drizzle/meta/0004_snapshot.json +7068 -0
  122. package/drizzle/meta/0005_snapshot.json +7180 -0
  123. package/drizzle/meta/0006_snapshot.json +7298 -0
  124. package/drizzle/meta/0007_snapshot.json +7620 -0
  125. package/drizzle/meta/0008_snapshot.json +7666 -0
  126. package/drizzle/meta/0009_snapshot.json +8185 -0
  127. package/drizzle/meta/0010_snapshot.json +8813 -0
  128. package/drizzle/meta/_journal.json +71 -1
  129. package/package.json +3 -2
  130. package/dist/repositories/dashboard.d.ts +0 -14
  131. package/dist/repositories/dashboard.js +0 -55
  132. package/dist/repositories/dashboard.js.map +0 -1
  133. package/dist/repositories/learn.d.ts +0 -19
  134. package/dist/repositories/learn.js +0 -54
  135. package/dist/repositories/learn.js.map +0 -1
  136. package/dist/repositories/students.d.ts +0 -15
  137. package/dist/repositories/students.js +0 -83
  138. package/dist/repositories/students.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","names":[],"sources":["../../src/repositories/content.ts"],"sourcesContent":["// content — Drizzle repository for the course aggregate root (implements the core\n// outbound `ContentRepository` port). Org-scoped: every method takes the domain\n// `organizations.id` and constrains its queries to that tenant. Rows come back\n// as stored — derived counts and cross-entity composition live in reporting.\nimport {\n eq,\n and,\n sql,\n count,\n asc,\n desc,\n ilike,\n or,\n inArray,\n type SQL,\n type AnyColumn,\n} from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { DbExecutor, Tx } from '../client.js';\nimport type {\n Activity,\n AddDownloadAssetInput,\n ContentRepository,\n Course,\n CourseStatus,\n CreateCourseInput,\n CreateDownloadInput,\n Download,\n DownloadAsset,\n DownloadStatus,\n ListCoursesQuery,\n ListDownloadsQuery,\n Module,\n Page,\n SaveActivityInput,\n UpdateCourseInput,\n UpdateDownloadInput,\n} from '@headless-lms/core/content';\nimport {\n contentItems,\n courses,\n modules,\n activities,\n activityAssets,\n downloads,\n downloadAssets,\n} from '../schema/content.js';\nimport { genId } from '@headless-lms/core/shared/id';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { NotFoundError, ConflictError } from '@headless-lms/core/shared/errors';\nimport { isUniqueViolation } from './pg-errors.js';\n\nfunction toCourse(row: typeof courses.$inferSelect): Course {\n return { ...row, status: row.status as CourseStatus };\n}\n\n// Columns a caller may sort by. Default falls back to createdAt desc.\nconst sortColumns: Record<string, AnyColumn | SQL> = {\n title: courses.title,\n slug: courses.slug,\n status: courses.status,\n category: courses.category,\n createdAt: courses.createdAt,\n updatedAt: courses.updatedAt,\n};\n\nconst downloadSortColumns: Record<string, AnyColumn | SQL> = {\n title: downloads.title,\n slug: downloads.slug,\n status: downloads.status,\n category: downloads.category,\n createdAt: downloads.createdAt,\n updatedAt: downloads.updatedAt,\n};\n\nfunction toDownload(row: typeof downloads.$inferSelect): Download {\n return { ...row, status: row.status as DownloadStatus };\n}\n\nexport class DrizzleContentRepository implements ContentRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it\n * nests as a savepoint inside the surrounding transaction. */\n private tx<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {\n return (this.db as NodePgDatabase).transaction(fn);\n }\n\n async list(orgId: string, query: ListCoursesQuery): Promise<Page<Course>> {\n const conditions: SQL[] = [eq(courses.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(courses.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(courses.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(courses.title, like), ilike(courses.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = 'createdAt';\n let direction: 'asc' | 'desc' = 'desc';\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in sortColumns) {\n sortKey = key;\n direction = isDesc ? 'desc' : 'asc';\n }\n }\n const sortExpr = sortColumns[sortKey] ?? courses.createdAt;\n const orderBy = direction === 'desc' ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(courses)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);\n\n return {\n rows: rows.map(toCourse),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Course | null> {\n const [row] = await this.db\n .select()\n .from(courses)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .limit(1);\n return row ? toCourse(row) : null;\n }\n\n async create(orgId: string, input: CreateCourseInput, slug: string): Promise<Course> {\n // Registry row + concrete row share one id. Both inserts run on the same\n // executor — mutations reach this repository tx-bound (ContentUnitOfWork),\n // so they commit or roll back together.\n const id = genId('course');\n await this.db.insert(contentItems).values({ orgId, id, type: 'course' });\n const [inserted] = await this.db\n .insert(courses)\n .values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? '',\n category: input.category ?? '',\n })\n .returning({ id: courses.id });\n if (!inserted) {\n throw new Error('failed to insert course');\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error('failed to load created course');\n }\n return created;\n }\n\n async update(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n // null is meaningful here — it clears the cover — so only `undefined` skips.\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n if (patch.settings !== undefined) {\n // Shallow merge, so a partial patch can't drop the keys it omits.\n set.settings = sql`${courses.settings} || ${JSON.stringify(patch.settings)}::jsonb`;\n }\n\n const [updated] = await this.db\n .update(courses)\n .set(set)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .returning({ id: courses.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n // Deletes go through the registry: cascades to the course row and its\n // entitlements in one statement. Never delete from `courses` directly —\n // that would strand the registry row.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, 'course')),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n // --- modules & activities ----------------------------------------------\n // Every mutation returns the course's full ordered module list (by `seq`),\n // so `listModules` runs at the end of every mutation within the same\n // transaction. An Activity's media is the many-to-many `activity_assets`\n // join: `saveActivity` upserts the row and replaces its asset links; deletes\n // drop the links first (they FK the activity), then the activity rows.\n\n listForCourse(orgId: string, courseId: string): Promise<Module[]> {\n return this.tx((tx) => this.listModules(tx, orgId, courseId));\n }\n\n async listActivitiesForCourse(orgId: string, courseId: string): Promise<Activity[]> {\n return this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activities.moduleId, activities.seq);\n }\n\n async findActivity(orgId: string, activityId: string): Promise<Activity | null> {\n const [row] = await this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)))\n .limit(1);\n return row ?? null;\n }\n\n async findModule(orgId: string, moduleId: string): Promise<Module | null> {\n const [row] = await this.db\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId)))\n .limit(1);\n return row ?? null;\n }\n\n /** The course's full ordered module list. */\n private async listModules(tx: Tx, orgId: string, courseId: string): Promise<Module[]> {\n return tx\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)))\n .orderBy(modules.seq);\n }\n\n /** Assert the module belongs to the org + course; throw otherwise. */\n private async assertModule(\n tx: Tx,\n orgId: string,\n courseId: string,\n moduleId: string,\n ): Promise<void> {\n const [row] = await tx\n .select({ id: modules.id })\n .from(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n )\n .limit(1);\n if (!row) {\n throw new NotFoundError('Module', moduleId);\n }\n }\n\n /** Replace an activity's ordered asset links with `assetIds`. */\n private async replaceActivityAssets(\n tx: Tx,\n orgId: string,\n activityId: string,\n assetIds: string[],\n ): Promise<void> {\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n if (assetIds.length) {\n await tx\n .insert(activityAssets)\n .values(assetIds.map((assetId, i) => ({ orgId, activityId, assetId, seq: i })));\n }\n }\n\n createModule(orgId: string, courseId: string, title: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n const existing = await tx\n .select({ seq: modules.seq })\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n await tx.insert(modules).values({ orgId, courseId, title, seq: nextSeq });\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n updateModule(\n orgId: string,\n courseId: string,\n moduleId: string,\n title: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n await tx\n .update(modules)\n .set({ title })\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the module's activities: their asset links first (they FK the\n // activity), then the activity rows, then the module.\n const activityRows = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const activityIds = activityRows.map((a) => a.id);\n\n if (activityIds.length) {\n await tx\n .delete(activityAssets)\n .where(\n and(eq(activityAssets.orgId, orgId), inArray(activityAssets.activityId, activityIds)),\n );\n await tx\n .delete(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n }\n\n await tx\n .delete(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]> {\n return this.tx(async (tx) => {\n // Two-phase to dodge the unique(org_id, id) is fine, but modules have no\n // unique(seq); still, park then assign to stay consistent with activities.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(modules)\n .set({ seq: i })\n .where(\n and(\n eq(modules.orgId, orgId),\n eq(modules.courseId, courseId),\n eq(modules.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderActivities(\n orgId: string,\n courseId: string,\n moduleId: string,\n orderedIds: string[],\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n // Two-phase to dodge the unique(org_id, module_id, seq) constraint mid-swap:\n // park rows at negative seqs, then assign the final 0..n-1.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: -(i + 1) })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: i })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n saveActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n input: SaveActivityInput,\n activityId?: string,\n ): Promise<{ modules: Module[]; activity: Activity }> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n let savedId: string;\n if (activityId) {\n const [existing] = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n )\n .limit(1);\n if (!existing) {\n throw new NotFoundError('Activity', activityId);\n }\n\n await tx\n .update(activities)\n .set({ settings: input.settings ?? null })\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)));\n if (input.assetIds !== undefined) {\n await this.replaceActivityAssets(tx, orgId, activityId, input.assetIds);\n }\n savedId = activityId;\n } else {\n const existing = await tx\n .select({ seq: activities.seq })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n\n const [ins] = await tx\n .insert(activities)\n .values({ orgId, moduleId, courseId, seq: nextSeq, settings: input.settings ?? null })\n .returning({ id: activities.id });\n if (!ins) {\n throw new Error('failed to insert activity');\n }\n await this.replaceActivityAssets(tx, orgId, ins.id, input.assetIds ?? []);\n savedId = ins.id;\n }\n\n const [activity] = await tx\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, savedId)))\n .limit(1);\n if (!activity) {\n throw new Error('failed to load saved activity');\n }\n return { modules: await this.listModules(tx, orgId, courseId), activity };\n });\n }\n\n deleteActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n activityId: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the asset links first (they FK the activity), then the activity.\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n await tx\n .delete(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n );\n\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n // --- downloads --------------------------------------------------------\n\n async listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>> {\n const conditions: SQL[] = [eq(downloads.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(downloads.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(downloads.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(downloads.title, like), ilike(downloads.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = 'createdAt';\n let direction: 'asc' | 'desc' = 'desc';\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in downloadSortColumns) {\n sortKey = key;\n direction = isDesc ? 'desc' : 'asc';\n }\n }\n const sortExpr = downloadSortColumns[sortKey] ?? downloads.createdAt;\n const orderBy = direction === 'desc' ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(downloads)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(downloads).where(where);\n\n return {\n rows: rows.map(toDownload),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getDownload(orgId: string, id: string): Promise<Download | null> {\n const [row] = await this.db\n .select()\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .limit(1);\n return row ? toDownload(row) : null;\n }\n\n async createDownload(\n orgId: string,\n input: CreateDownloadInput,\n slug: string,\n ): Promise<Download> {\n // Registry row + concrete row share one id, same as courses. Both inserts\n // run on the same executor — mutations reach this repository tx-bound.\n const id = genId('download');\n await this.db.insert(contentItems).values({ orgId, id, type: 'download' });\n await this.db.insert(downloads).values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? '',\n category: input.category ?? '',\n });\n const created = await this.getDownload(orgId, id);\n if (!created) {\n throw new Error('failed to load created download');\n }\n return created;\n }\n\n async updateDownload(\n orgId: string,\n id: string,\n patch: UpdateDownloadInput,\n ): Promise<Download | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(downloads)\n .set(set)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .returning({ id: downloads.id });\n if (!updated) {\n return null;\n }\n return this.getDownload(orgId, id);\n }\n\n async deleteDownload(orgId: string, id: string): Promise<boolean> {\n // Through the registry, so the cascade reaches the download row, its asset\n // links, and the grants. Never delete from `downloads` directly.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, 'download'),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n async listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]> {\n return this.db\n .select()\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)))\n .orderBy(asc(downloadAssets.seq));\n }\n\n /** Assert the download exists in the org; throw otherwise. */\n private async assertDownload(orgId: string, downloadId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: downloads.id })\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, downloadId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError('Download', downloadId);\n }\n }\n\n async addDownloadAsset(\n orgId: string,\n downloadId: string,\n input: AddDownloadAssetInput,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n const [maxRow] = await this.db\n .select({ maxSeq: sql<number | null>`max(${downloadAssets.seq})` })\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)));\n const nextSeq = (maxRow?.maxSeq ?? -1) + 1;\n try {\n await this.db.insert(downloadAssets).values({\n orgId,\n downloadId,\n assetId: input.assetId,\n seq: nextSeq,\n displayName: input.displayName ?? null,\n });\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError('This asset is already linked to the download');\n }\n throw err;\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async removeDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .delete(downloadAssets)\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async renameDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n displayName: string | null,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .update(downloadAssets)\n .set({ displayName })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async reorderDownloadAssets(\n orgId: string,\n downloadId: string,\n assetIds: string[],\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n // The service has already verified this is exactly the current set.\n for (const [seq, assetId] of assetIds.entries()) {\n await this.db\n .update(downloadAssets)\n .set({ seq })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"content.js","names":[],"sources":["../../src/repositories/content.ts"],"sourcesContent":["// content — Drizzle repository for the course aggregate root (implements the core\n// outbound `ContentRepository` port). Org-scoped: every method takes the domain\n// `organizations.id` and constrains its queries to that tenant. Rows come back\n// as stored — derived counts and cross-entity composition live in reporting.\nimport {\n eq,\n and,\n sql,\n count,\n asc,\n desc,\n ilike,\n or,\n inArray,\n type SQL,\n type AnyColumn,\n} from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { DbExecutor, Tx } from \"../client.js\";\nimport type {\n Activity,\n ActivityAsset,\n AddDownloadAssetInput,\n Bundle,\n BundleItem,\n Community,\n CommunityStatus,\n ContentRepository,\n Course,\n CourseSettings,\n CourseStatus,\n CreateBundleInput,\n CreateCommunityInput,\n CreateCourseInput,\n CreateDownloadInput,\n CreateSpaceInput,\n Download,\n DownloadAsset,\n DownloadStatus,\n ListBundlesQuery,\n ListCommunitiesQuery,\n ListCoursesQuery,\n ListDownloadsQuery,\n Module,\n Page,\n PatchDmSettingsInput,\n SaveActivityInput,\n SetSpaceTopicsInput,\n Space,\n SpaceAccessRule,\n SpaceDetail,\n UpdateBundleInput,\n UpdateCommunityInput,\n UpdateCourseInput,\n UpdateDownloadInput,\n UpdateSpaceInput,\n} from \"@headless-lms/core/content\";\nimport {\n communitySettingsDefaults,\n courseSettingsDefaults,\n spaceSettingsDefaults,\n type Setting,\n} from \"@headless-lms/core/schemas\";\nimport {\n bundles,\n bundleItems,\n communities,\n contentItems,\n courses,\n modules,\n activities,\n activityAssets,\n downloads,\n downloadAssets,\n spaceLinks,\n spaceTopics,\n spaces,\n} from \"../schema/content.js\";\nimport { settings } from \"../schema/settings.js\";\nimport { posts } from \"../schema/discussion.js\";\nimport { deepMerge } from \"./settings.js\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { NotFoundError, ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst LIBRARY_ONLY = \"An activity can only use ready files from the media library\";\nconst MISSING_ASSET_DETAIL = /^Key \\(org_id, asset_id\\)=\\((?:[^,]+), (.+?)\\) is not present/;\n\nfunction refusedLink(err: unknown): ConflictError | undefined {\n const pg = pgErrorFields(err);\n if (pg?.code !== \"23503\") {\n return undefined;\n }\n const missing = pg.detail?.match(MISSING_ASSET_DETAIL)?.[1];\n const message = missing ? `${LIBRARY_ONLY}. Not available: ${missing}` : LIBRARY_ONLY;\n return Object.assign(new ConflictError(message), { cause: err });\n}\n\n// A course's settings live in the shared `settings` table (namespace\n// 'content', scoped by course id) — joined here so the repository answers in\n// whole courses regardless of where the pieces are stored.\nconst SETTINGS_NAMESPACE = \"content\";\n\nconst courseSettingsJoin = and(\n eq(settings.orgId, courses.orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, courses.id),\n);\n\nconst communitySettingsJoin = and(\n eq(settings.orgId, communities.orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, communities.id),\n);\n\nfunction toCommunity(\n row: typeof communities.$inferSelect,\n storedSettings: Record<string, unknown> | null,\n): Community {\n const raw = storedSettings?.dm;\n const stored =\n raw && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n return {\n ...row,\n status: row.status as CommunityStatus,\n settings: { dm: { ...communitySettingsDefaults.dm, ...stored } as Community[\"settings\"][\"dm\"] },\n };\n}\n\nfunction toSpace(row: typeof spaces.$inferSelect): Space {\n return {\n ...row,\n accessRule: row.accessRule as SpaceAccessRule,\n settings: { ...spaceSettingsDefaults, ...row.settings },\n };\n}\n\nconst communitySortColumns: Record<string, AnyColumn | SQL> = {\n title: communities.title,\n slug: communities.slug,\n status: communities.status,\n createdAt: communities.createdAt,\n updatedAt: communities.updatedAt,\n};\n\nfunction toCourse(\n row: typeof courses.$inferSelect,\n storedSettings: Record<string, unknown> | null,\n): Course {\n return {\n ...row,\n status: row.status as CourseStatus,\n settings: { ...courseSettingsDefaults, ...storedSettings } as Course[\"settings\"],\n };\n}\n\n// Columns a caller may sort by. Default falls back to createdAt desc.\nconst sortColumns: Record<string, AnyColumn | SQL> = {\n title: courses.title,\n slug: courses.slug,\n status: courses.status,\n category: courses.category,\n createdAt: courses.createdAt,\n updatedAt: courses.updatedAt,\n};\n\nconst downloadSortColumns: Record<string, AnyColumn | SQL> = {\n title: downloads.title,\n slug: downloads.slug,\n status: downloads.status,\n category: downloads.category,\n createdAt: downloads.createdAt,\n updatedAt: downloads.updatedAt,\n};\n\nfunction toDownload(row: typeof downloads.$inferSelect): Download {\n return { ...row, status: row.status as DownloadStatus };\n}\n\nconst bundleSortColumns: Record<string, AnyColumn | SQL> = {\n name: bundles.name,\n createdAt: bundles.createdAt,\n updatedAt: bundles.updatedAt,\n};\n\nexport class DrizzleContentRepository implements ContentRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it\n * nests as a savepoint inside the surrounding transaction. */\n private tx<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {\n return (this.db as NodePgDatabase).transaction(fn);\n }\n\n async listCourses(orgId: string, query: ListCoursesQuery): Promise<Page<Course>> {\n const conditions: SQL[] = [eq(courses.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(courses.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(courses.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(courses.title, like), ilike(courses.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in sortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = sortColumns[sortKey] ?? courses.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select({ course: courses, settingsValue: settings.value })\n .from(courses)\n .leftJoin(settings, courseSettingsJoin)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);\n\n return {\n rows: rows.map((row) => toCourse(row.course, row.settingsValue)),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findCourseById(orgId: string, id: string): Promise<Course | null> {\n const [row] = await this.db\n .select({ course: courses, settingsValue: settings.value })\n .from(courses)\n .leftJoin(settings, courseSettingsJoin)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .limit(1);\n return row ? toCourse(row.course, row.settingsValue) : null;\n }\n\n async createCourse(orgId: string, input: CreateCourseInput, slug: string): Promise<Course> {\n // Registry row + concrete row share one id. Both inserts run on the same\n // executor — mutations reach this repository tx-bound (ContentUnitOfWork),\n // so they commit or roll back together.\n const id = genId(\"course\");\n await this.db.insert(contentItems).values({ orgId, id, type: \"course\" });\n const [inserted] = await this.db\n .insert(courses)\n .values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? \"\",\n category: input.category ?? \"\",\n })\n .returning({ id: courses.id });\n if (!inserted) {\n throw new Error(\"failed to insert course\");\n }\n const created = await this.findCourseById(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created course\");\n }\n return created;\n }\n\n async updateCourse(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n // null is meaningful here — it clears the cover — so only `undefined` skips.\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(courses)\n .set(set)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .returning({ id: courses.id });\n if (!updated) {\n return null;\n }\n return this.findCourseById(orgId, id);\n }\n\n async patchCourseSettings(\n orgId: string,\n id: string,\n value: Partial<CourseSettings>,\n ): Promise<CourseSettings> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, id),\n );\n\n // Merge in JS under a row lock, same as the settings repository: the read\n // and the write must be one atomic unit or concurrent patches would\n // clobber each other's sibling keys.\n const merged = await this.tx(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for(\"update\");\n\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({\n orgId,\n namespace: SETTINGS_NAMESPACE,\n scopeId: id,\n value: value as Setting[\"value\"],\n })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted.value;\n }\n [existing] = await tx.select().from(settings).where(key).for(\"update\");\n if (!existing) {\n throw new Error(\"course settings patch: row vanished mid-transaction\");\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, value) as Setting[\"value\"], updatedAt: new Date() })\n .where(key)\n .returning();\n if (!updated) {\n throw new Error(\"course settings patch returned no row\");\n }\n return updated.value;\n });\n\n return { ...courseSettingsDefaults, ...merged } as CourseSettings;\n }\n\n async deleteCourse(orgId: string, id: string): Promise<boolean> {\n // Deletes go through the registry: cascades to the course row and its\n // entitlements in one statement. Never delete from `courses` directly —\n // that would strand the registry row.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, \"course\"),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n // --- modules & activities ----------------------------------------------\n // Every mutation returns the course's full ordered module list (by `seq`),\n // so `listModules` runs at the end of every mutation within the same\n // transaction. An Activity's media is the many-to-many `activity_assets`\n // join: `saveActivity` upserts the row and replaces its asset links; deletes\n // drop the links first (they FK the activity), then the activity rows.\n\n listCourseModules(orgId: string, courseId: string): Promise<Module[]> {\n return this.tx((tx) => this.listModules(tx, orgId, courseId));\n }\n\n async listActivitiesForCourse(orgId: string, courseId: string): Promise<Activity[]> {\n return this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activities.moduleId, activities.seq);\n }\n\n // Bare join-table rows; the activities join only scopes them to the course.\n async listActivityAssetsForCourse(orgId: string, courseId: string): Promise<ActivityAsset[]> {\n return this.db\n .select({\n orgId: activityAssets.orgId,\n id: activityAssets.id,\n activityId: activityAssets.activityId,\n assetId: activityAssets.assetId,\n seq: activityAssets.seq,\n createdAt: activityAssets.createdAt,\n updatedAt: activityAssets.updatedAt,\n })\n .from(activityAssets)\n .innerJoin(\n activities,\n and(\n eq(activities.orgId, activityAssets.orgId),\n eq(activities.id, activityAssets.activityId),\n ),\n )\n .where(and(eq(activityAssets.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activityAssets.activityId, activityAssets.seq);\n }\n\n async findActivity(orgId: string, activityId: string): Promise<Activity | null> {\n const [row] = await this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)))\n .limit(1);\n return row ?? null;\n }\n\n async findModule(orgId: string, moduleId: string): Promise<Module | null> {\n const [row] = await this.db\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId)))\n .limit(1);\n return row ?? null;\n }\n\n /** The course's full ordered module list. */\n private async listModules(tx: Tx, orgId: string, courseId: string): Promise<Module[]> {\n return tx\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)))\n .orderBy(modules.seq);\n }\n\n /** Assert the module belongs to the org + course; throw otherwise. */\n private async assertModule(\n tx: Tx,\n orgId: string,\n courseId: string,\n moduleId: string,\n ): Promise<void> {\n const [row] = await tx\n .select({ id: modules.id })\n .from(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n )\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Module\", moduleId);\n }\n }\n\n /** Replace an activity's ordered asset links with `assetIds`. */\n private async replaceActivityAssets(\n tx: Tx,\n orgId: string,\n activityId: string,\n assetIds: string[],\n ): Promise<void> {\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n if (assetIds.length) {\n try {\n await tx\n .insert(activityAssets)\n .values(assetIds.map((assetId, i) => ({ orgId, activityId, assetId, seq: i })));\n } catch (err) {\n throw refusedLink(err) ?? err;\n }\n }\n }\n\n createModule(orgId: string, courseId: string, title: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n const existing = await tx\n .select({ seq: modules.seq })\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n await tx.insert(modules).values({ orgId, courseId, title, seq: nextSeq });\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n updateModule(\n orgId: string,\n courseId: string,\n moduleId: string,\n title: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n await tx\n .update(modules)\n .set({ title })\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the module's activities: their asset links first (they FK the\n // activity), then the activity rows, then the module.\n const activityRows = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const activityIds = activityRows.map((a) => a.id);\n\n if (activityIds.length) {\n await tx\n .delete(activityAssets)\n .where(\n and(eq(activityAssets.orgId, orgId), inArray(activityAssets.activityId, activityIds)),\n );\n await tx\n .delete(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n }\n\n await tx\n .delete(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]> {\n return this.tx(async (tx) => {\n // Two-phase to dodge the unique(org_id, id) is fine, but modules have no\n // unique(seq); still, park then assign to stay consistent with activities.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(modules)\n .set({ seq: i })\n .where(\n and(\n eq(modules.orgId, orgId),\n eq(modules.courseId, courseId),\n eq(modules.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderActivities(\n orgId: string,\n courseId: string,\n moduleId: string,\n orderedIds: string[],\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n // Two-phase to dodge the unique(org_id, module_id, seq) constraint mid-swap:\n // park rows at negative seqs, then assign the final 0..n-1.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: -(i + 1) })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: i })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n saveActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n input: SaveActivityInput,\n activityId?: string,\n ): Promise<{ modules: Module[]; activity: Activity }> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n let savedId: string;\n if (activityId) {\n const [existing] = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n )\n .limit(1);\n if (!existing) {\n throw new NotFoundError(\"Activity\", activityId);\n }\n\n await tx\n .update(activities)\n .set({ settings: input.settings ?? null })\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)));\n if (input.assetIds !== undefined) {\n await this.replaceActivityAssets(tx, orgId, activityId, input.assetIds);\n }\n savedId = activityId;\n } else {\n const existing = await tx\n .select({ seq: activities.seq })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n\n const [ins] = await tx\n .insert(activities)\n .values({ orgId, moduleId, courseId, seq: nextSeq, settings: input.settings ?? null })\n .returning({ id: activities.id });\n if (!ins) {\n throw new Error(\"failed to insert activity\");\n }\n await this.replaceActivityAssets(tx, orgId, ins.id, input.assetIds ?? []);\n savedId = ins.id;\n }\n\n const [activity] = await tx\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, savedId)))\n .limit(1);\n if (!activity) {\n throw new Error(\"failed to load saved activity\");\n }\n return { modules: await this.listModules(tx, orgId, courseId), activity };\n });\n }\n\n deleteActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n activityId: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the asset links first (they FK the activity), then the activity.\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n await tx\n .delete(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n );\n\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n // --- downloads --------------------------------------------------------\n\n async listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>> {\n const conditions: SQL[] = [eq(downloads.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(downloads.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(downloads.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(downloads.title, like), ilike(downloads.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in downloadSortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = downloadSortColumns[sortKey] ?? downloads.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(downloads)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(downloads).where(where);\n\n return {\n rows: rows.map(toDownload),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getDownload(orgId: string, id: string): Promise<Download | null> {\n const [row] = await this.db\n .select()\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .limit(1);\n return row ? toDownload(row) : null;\n }\n\n async createDownload(orgId: string, input: CreateDownloadInput, slug: string): Promise<Download> {\n // Registry row + concrete row share one id, same as courses. Both inserts\n // run on the same executor — mutations reach this repository tx-bound.\n const id = genId(\"download\");\n await this.db.insert(contentItems).values({ orgId, id, type: \"download\" });\n await this.db.insert(downloads).values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? \"\",\n category: input.category ?? \"\",\n });\n const created = await this.getDownload(orgId, id);\n if (!created) {\n throw new Error(\"failed to load created download\");\n }\n return created;\n }\n\n async updateDownload(\n orgId: string,\n id: string,\n patch: UpdateDownloadInput,\n ): Promise<Download | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(downloads)\n .set(set)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .returning({ id: downloads.id });\n if (!updated) {\n return null;\n }\n return this.getDownload(orgId, id);\n }\n\n async deleteDownload(orgId: string, id: string): Promise<boolean> {\n // Through the registry, so the cascade reaches the download row, its asset\n // links, and the grants. Never delete from `downloads` directly.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, \"download\"),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n async listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]> {\n return this.db\n .select()\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)))\n .orderBy(asc(downloadAssets.seq));\n }\n\n /** Assert the download exists in the org; throw otherwise. */\n private async assertDownload(orgId: string, downloadId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: downloads.id })\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, downloadId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Download\", downloadId);\n }\n }\n\n async addDownloadAsset(\n orgId: string,\n downloadId: string,\n input: AddDownloadAssetInput,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n const [maxRow] = await this.db\n .select({ maxSeq: sql<number | null>`max(${downloadAssets.seq})` })\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)));\n const nextSeq = (maxRow?.maxSeq ?? -1) + 1;\n try {\n await this.db.insert(downloadAssets).values({\n orgId,\n downloadId,\n assetId: input.assetId,\n seq: nextSeq,\n displayName: input.displayName ?? null,\n });\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"This asset is already linked to the download\");\n }\n throw err;\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async removeDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .delete(downloadAssets)\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async renameDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n displayName: string | null,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .update(downloadAssets)\n .set({ displayName })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async reorderDownloadAssets(\n orgId: string,\n downloadId: string,\n assetIds: string[],\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n // The service has already verified this is exactly the current set.\n for (const [seq, assetId] of assetIds.entries()) {\n await this.db\n .update(downloadAssets)\n .set({ seq })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n // --- bundles ----------------------------------------------------------\n\n async listBundles(orgId: string, query: ListBundlesQuery): Promise<Page<Bundle>> {\n const conditions: SQL[] = [eq(bundles.orgId, orgId)];\n\n const search = query.search?.trim();\n if (search) {\n conditions.push(ilike(bundles.name, `%${search}%`));\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in bundleSortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = bundleSortColumns[sortKey] ?? bundles.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(bundles)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(bundles).where(where);\n\n return {\n rows,\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findBundleById(orgId: string, id: string): Promise<Bundle | null> {\n const [row] = await this.db\n .select()\n .from(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .limit(1);\n return row ?? null;\n }\n\n /** The bundle and its initial items are one write: an unknown content id\n * rolls the bundle back rather than leaving it created and half-filled. */\n async createBundle(orgId: string, input: CreateBundleInput): Promise<Bundle> {\n const contentIds = [...new Set(input.contentIds ?? [])];\n return this.tx(async (tx) => {\n // A bundle is not content: it groups registry rows, so it gets no\n // content_items row of its own.\n const [row] = await tx\n .insert(bundles)\n .values({ orgId, id: genId(\"bundle\"), name: input.name })\n .returning();\n if (!row) {\n throw new Error(\"failed to load created bundle\");\n }\n\n if (contentIds.length > 0) {\n // Against the registry, so any content type can be bundled.\n const found = await tx\n .select({ id: contentItems.id })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), inArray(contentItems.id, contentIds)));\n const known = new Set(found.map((item) => item.id));\n const missing = contentIds.find((id) => !known.has(id));\n if (missing) {\n throw new NotFoundError(\"Content\", missing);\n }\n\n await tx\n .insert(bundleItems)\n .values(contentIds.map((contentId) => ({ orgId, bundleId: row.id, contentId })));\n }\n\n return row;\n });\n }\n\n async updateBundle(orgId: string, id: string, patch: UpdateBundleInput): Promise<Bundle | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n\n const [row] = await this.db\n .update(bundles)\n .set(set)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .returning();\n return row ?? null;\n }\n\n async deleteBundle(orgId: string, id: string): Promise<boolean> {\n // Cascades to the bundle's items and to the grants made on it. The content\n // itself is untouched — a bundle only groups registry rows.\n const deleted = await this.db\n .delete(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .returning({ id: bundles.id });\n return deleted.length > 0;\n }\n\n async listBundleItems(orgId: string, bundleId: string): Promise<BundleItem[]> {\n return this.db\n .select()\n .from(bundleItems)\n .where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId)))\n .orderBy(asc(bundleItems.createdAt));\n }\n\n /** Assert the bundle exists in the org; throw otherwise. */\n private async assertBundle(orgId: string, bundleId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: bundles.id })\n .from(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, bundleId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Bundle\", bundleId);\n }\n }\n\n async addBundleItem(orgId: string, bundleId: string, contentId: string): Promise<BundleItem[]> {\n await this.assertBundle(orgId, bundleId);\n // Against the registry, so any content type can be bundled.\n const [item] = await this.db\n .select({ id: contentItems.id })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId)))\n .limit(1);\n if (!item) {\n throw new NotFoundError(\"Content\", contentId);\n }\n await this.db\n .insert(bundleItems)\n .values({ orgId, bundleId, contentId })\n .onConflictDoNothing({\n target: [bundleItems.orgId, bundleItems.bundleId, bundleItems.contentId],\n });\n return this.listBundleItems(orgId, bundleId);\n }\n\n async removeBundleItem(\n orgId: string,\n bundleId: string,\n contentId: string,\n ): Promise<BundleItem[]> {\n await this.assertBundle(orgId, bundleId);\n await this.db\n .delete(bundleItems)\n .where(\n and(\n eq(bundleItems.orgId, orgId),\n eq(bundleItems.bundleId, bundleId),\n eq(bundleItems.contentId, contentId),\n ),\n );\n return this.listBundleItems(orgId, bundleId);\n }\n\n async listCommunities(orgId: string, query: ListCommunitiesQuery): Promise<Page<Community>> {\n const conditions: SQL[] = [eq(communities.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(communities.status, query.status));\n }\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(communities.title, like), ilike(communities.description, like));\n if (match) {\n conditions.push(match);\n }\n }\n const where = and(...conditions);\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in communitySortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = communitySortColumns[sortKey] ?? communities.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n const rows = await this.db\n .select({ community: communities, settingsValue: settings.value })\n .from(communities)\n .leftJoin(settings, communitySettingsJoin)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [totalRow] = await this.db.select({ value: count() }).from(communities).where(where);\n return {\n rows: rows.map((row) => toCommunity(row.community, row.settingsValue)),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findCommunityById(orgId: string, id: string): Promise<Community | null> {\n const [row] = await this.db\n .select({ community: communities, settingsValue: settings.value })\n .from(communities)\n .leftJoin(settings, communitySettingsJoin)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, id)))\n .limit(1);\n return row ? toCommunity(row.community, row.settingsValue) : null;\n }\n\n async findCommunitiesByIds(orgId: string, ids: string[]): Promise<Community[]> {\n const rows = await this.db\n .select({ community: communities, settingsValue: settings.value })\n .from(communities)\n .leftJoin(settings, communitySettingsJoin)\n .where(and(eq(communities.orgId, orgId), inArray(communities.id, ids)))\n .orderBy(asc(communities.title), asc(communities.id));\n return rows.map((row) => toCommunity(row.community, row.settingsValue));\n }\n\n async listPublishedCommunityIds(orgId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(and(eq(communities.orgId, orgId), eq(communities.status, \"published\")))\n .orderBy(asc(communities.createdAt), asc(communities.id));\n return rows.map((row) => row.id);\n }\n\n async createCommunity(\n orgId: string,\n input: CreateCommunityInput,\n slug: string,\n ): Promise<Community> {\n const id = genId(\"community\");\n await this.db.insert(contentItems).values({ orgId, id, type: \"community\" });\n const [inserted] = await this.db\n .insert(communities)\n .values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? \"\",\n })\n .returning({ id: communities.id });\n if (!inserted) {\n throw new Error(\"failed to insert community\");\n }\n const created = await this.findCommunityById(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created community\");\n }\n return created;\n }\n\n async updateCommunity(\n orgId: string,\n id: string,\n patch: UpdateCommunityInput,\n ): Promise<Community | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n const [updated] = await this.db\n .update(communities)\n .set(set)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, id)))\n .returning({ id: communities.id });\n if (!updated) {\n return null;\n }\n return this.findCommunityById(orgId, id);\n }\n\n async patchDmSettings(\n orgId: string,\n id: string,\n patch: PatchDmSettingsInput,\n ): Promise<Community | null> {\n const community = await this.findCommunityById(orgId, id);\n if (!community) {\n return null;\n }\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, id),\n );\n await this.tx(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for(\"update\");\n const value = { dm: patch } as Setting[\"value\"];\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({ orgId, namespace: SETTINGS_NAMESPACE, scopeId: id, value })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return;\n }\n [existing] = await tx.select().from(settings).where(key).for(\"update\");\n if (!existing) {\n throw new Error(\"community settings patch: row vanished mid-transaction\");\n }\n }\n await tx\n .update(settings)\n .set({\n value: deepMerge(existing.value, { dm: patch }) as Setting[\"value\"],\n updatedAt: new Date(),\n })\n .where(key);\n });\n return this.findCommunityById(orgId, id);\n }\n\n async deleteCommunity(orgId: string, id: string): Promise<boolean> {\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, \"community\"),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n async deleteCommunitySettings(orgId: string, id: string): Promise<void> {\n await this.db\n .delete(settings)\n .where(\n and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, id),\n ),\n );\n }\n\n async listSpaces(orgId: string, communityId: string): Promise<SpaceDetail[]> {\n const rows = await this.db\n .select()\n .from(spaces)\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)))\n .orderBy(asc(spaces.seq));\n return this.withSpaceParts(orgId, rows.map(toSpace));\n }\n\n async findSpace(orgId: string, spaceId: string): Promise<SpaceDetail | null> {\n const [row] = await this.db\n .select()\n .from(spaces)\n .where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId)))\n .limit(1);\n if (!row) {\n return null;\n }\n const [detail] = await this.withSpaceParts(orgId, [toSpace(row)]);\n return detail ?? null;\n }\n\n async lockSpace(orgId: string, spaceId: string): Promise<Space | null> {\n const [row] = await this.db\n .select()\n .from(spaces)\n .where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId)))\n .for(\"update\")\n .limit(1);\n return row ? toSpace(row) : null;\n }\n\n async createSpace(\n orgId: string,\n communityId: string,\n input: CreateSpaceInput,\n ): Promise<SpaceDetail> {\n const existing = await this.db\n .select({ seq: spaces.seq })\n .from(spaces)\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)));\n const nextSeq = existing.reduce((max, row) => Math.max(max, row.seq), -1) + 1;\n const [inserted] = await this.db\n .insert(spaces)\n .values({\n orgId,\n communityId,\n name: input.name,\n description: input.description ?? \"\",\n seq: nextSeq,\n accessRule: input.accessRule ?? \"all_members\",\n capacity: input.capacity ?? null,\n settings: { ...spaceSettingsDefaults, ...input.settings },\n })\n .returning({ id: spaces.id });\n if (!inserted) {\n throw new Error(\"failed to insert space\");\n }\n const created = await this.findSpace(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created space\");\n }\n return created;\n }\n\n async updateSpace(\n orgId: string,\n communityId: string,\n spaceId: string,\n patch: UpdateSpaceInput,\n ): Promise<SpaceDetail | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.accessRule !== undefined) {\n set.accessRule = patch.accessRule;\n }\n if (patch.capacity !== undefined) {\n set.capacity = patch.capacity;\n }\n if (patch.settings !== undefined) {\n set.settings = { ...spaceSettingsDefaults, ...patch.settings };\n }\n const [updated] = await this.db\n .update(spaces)\n .set(set)\n .where(\n and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId), eq(spaces.communityId, communityId)),\n )\n .returning({ id: spaces.id });\n if (!updated) {\n return null;\n }\n return this.findSpace(orgId, spaceId);\n }\n\n async deleteSpace(orgId: string, communityId: string, spaceId: string): Promise<boolean> {\n const deleted = await this.db\n .delete(spaces)\n .where(\n and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId), eq(spaces.communityId, communityId)),\n )\n .returning({ id: spaces.id });\n return deleted.length > 0;\n }\n\n async reorderSpaces(\n orgId: string,\n communityId: string,\n orderedIds: string[],\n ): Promise<SpaceDetail[]> {\n for (let i = 0; i < orderedIds.length; i++) {\n await this.db\n .update(spaces)\n .set({ seq: i })\n .where(\n and(\n eq(spaces.orgId, orgId),\n eq(spaces.communityId, communityId),\n eq(spaces.id, orderedIds[i]!),\n ),\n );\n }\n return this.listSpaces(orgId, communityId);\n }\n\n async setSpaceTopics(\n orgId: string,\n spaceId: string,\n input: SetSpaceTopicsInput,\n ): Promise<SpaceDetail> {\n await this.tx(async (tx) => {\n const existing = await tx\n .select({ id: spaceTopics.id })\n .from(spaceTopics)\n .where(and(eq(spaceTopics.orgId, orgId), eq(spaceTopics.spaceId, spaceId)))\n .for(\"update\");\n const kept = new Set(input.topics.flatMap((topic) => (topic.id ? [topic.id] : [])));\n const known = new Set(existing.map((topic) => topic.id));\n for (const id of kept) {\n if (!known.has(id)) {\n throw new NotFoundError(\"Topic\", id);\n }\n }\n const removed = existing.filter((topic) => !kept.has(topic.id)).map((topic) => topic.id);\n if (removed.length) {\n await tx\n .delete(spaceTopics)\n .where(and(eq(spaceTopics.orgId, orgId), inArray(spaceTopics.id, removed)));\n }\n for (let seq = 0; seq < input.topics.length; seq++) {\n const topic = input.topics[seq]!;\n if (topic.id) {\n await tx\n .update(spaceTopics)\n .set({ name: topic.name, seq, updatedAt: new Date() })\n .where(and(eq(spaceTopics.orgId, orgId), eq(spaceTopics.id, topic.id)));\n } else {\n await tx.insert(spaceTopics).values({ orgId, spaceId, name: topic.name, seq });\n }\n }\n });\n const space = await this.findSpace(orgId, spaceId);\n if (!space) {\n throw new NotFoundError(\"Space\", spaceId);\n }\n return space;\n }\n\n async topicsInUse(orgId: string, spaceId: string, topicIds: string[]): Promise<string[]> {\n if (topicIds.length === 0) {\n return [];\n }\n await this.db\n .select({ id: spaceTopics.id })\n .from(spaceTopics)\n .where(\n and(\n eq(spaceTopics.orgId, orgId),\n eq(spaceTopics.spaceId, spaceId),\n inArray(spaceTopics.id, topicIds),\n ),\n )\n .for(\"update\");\n const rows = await this.db\n .selectDistinct({ topicId: posts.topicId })\n .from(posts)\n .where(\n and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), inArray(posts.topicId, topicIds)),\n );\n return rows.flatMap((row) => (row.topicId ? [row.topicId] : []));\n }\n\n async addSpaceLink(orgId: string, spaceId: string, contentId: string): Promise<SpaceDetail> {\n const [item] = await this.db\n .select({ id: contentItems.id, type: contentItems.type })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId)))\n .limit(1);\n if (\n !item ||\n (item.type !== \"course\" && item.type !== \"download\" && item.type !== \"community\")\n ) {\n throw new NotFoundError(\"Content\", contentId);\n }\n const existing = await this.db\n .select({ seq: spaceLinks.seq })\n .from(spaceLinks)\n .where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.spaceId, spaceId)));\n const nextSeq = existing.reduce((max, row) => Math.max(max, row.seq), -1) + 1;\n await this.db.insert(spaceLinks).values({ orgId, spaceId, contentId, seq: nextSeq });\n const space = await this.findSpace(orgId, spaceId);\n if (!space) {\n throw new NotFoundError(\"Space\", spaceId);\n }\n return space;\n }\n\n async removeSpaceLink(orgId: string, spaceId: string, linkId: string): Promise<SpaceDetail> {\n const deleted = await this.db\n .delete(spaceLinks)\n .where(\n and(\n eq(spaceLinks.orgId, orgId),\n eq(spaceLinks.spaceId, spaceId),\n eq(spaceLinks.id, linkId),\n ),\n )\n .returning({ id: spaceLinks.id });\n if (deleted.length === 0) {\n throw new NotFoundError(\"Link\", linkId);\n }\n const space = await this.findSpace(orgId, spaceId);\n if (!space) {\n throw new NotFoundError(\"Space\", spaceId);\n }\n return space;\n }\n\n async reorderSpaceLinks(\n orgId: string,\n spaceId: string,\n orderedIds: string[],\n ): Promise<SpaceDetail> {\n for (let i = 0; i < orderedIds.length; i++) {\n await this.db\n .update(spaceLinks)\n .set({ seq: i })\n .where(\n and(\n eq(spaceLinks.orgId, orgId),\n eq(spaceLinks.spaceId, spaceId),\n eq(spaceLinks.id, orderedIds[i]!),\n ),\n );\n }\n const space = await this.findSpace(orgId, spaceId);\n if (!space) {\n throw new NotFoundError(\"Space\", spaceId);\n }\n return space;\n }\n\n async spacesLinkingContent(orgId: string, contentId: string): Promise<string[]> {\n const rows = await this.db\n .select({ spaceId: spaceLinks.spaceId })\n .from(spaceLinks)\n .where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.contentId, contentId)));\n return [...new Set(rows.map((row) => row.spaceId))];\n }\n\n private async withSpaceParts(orgId: string, rows: Space[]): Promise<SpaceDetail[]> {\n if (rows.length === 0) {\n return [];\n }\n const ids = rows.map((row) => row.id);\n const topics = await this.db\n .select()\n .from(spaceTopics)\n .where(and(eq(spaceTopics.orgId, orgId), inArray(spaceTopics.spaceId, ids)))\n .orderBy(asc(spaceTopics.seq));\n const links = await this.db\n .select()\n .from(spaceLinks)\n .where(and(eq(spaceLinks.orgId, orgId), inArray(spaceLinks.spaceId, ids)))\n .orderBy(asc(spaceLinks.seq));\n return rows.map((space) => ({\n ...space,\n topics: topics.filter((topic) => topic.spaceId === space.id),\n links: links.filter((link) => link.spaceId === space.id),\n }));\n }\n}\ntranslateDbErrors(DrizzleContentRepository);\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { credentials } from "../schema/credentials.js";
2
+ import { translateDbErrors } from "./pg-errors.js";
2
3
  import { and, eq } from "drizzle-orm";
3
4
  import { genId } from "@headless-lms/core/shared/id";
4
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
@@ -80,6 +81,7 @@ var DrizzleCredentialStore = class {
80
81
  await this.db.delete(credentials).where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));
81
82
  }
82
83
  };
84
+ translateDbErrors(DrizzleCredentialStore);
83
85
  //#endregion
84
86
  export { DrizzleCredentialStore, decryptSecret, encryptSecret };
85
87
 
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","names":[],"sources":["../../src/repositories/credentials.ts"],"sourcesContent":["// credentials — Drizzle-backed secure credential store (implements the shared\n// core port). Secrets are JSON documents: serialized here, encrypted with\n// AES-256-GCM via node:crypto (single 32-byte key from the environment), and\n// parsed back to objects on reveal — callers never (de)serialize. The AAD\n// binds each ciphertext to its (org, id) row, so moving a ciphertext to\n// another row or org makes it undecryptable. Plaintext exists only inside\n// store/reveal — it is never logged, listed, or returned elsewhere.\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { and, eq } from 'drizzle-orm';\nimport type { DbExecutor } from '../client.js';\nimport type { CredentialStore, Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { credentials } from '../schema/credentials.js';\n\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\n/** Version of the key currently used for new writes (stored per row for rotation). */\nconst KEY_VERSION = 1;\n\nfunction aad(orgId: string, id: string): Buffer {\n return Buffer.from(`${orgId}:${id}`, 'utf8');\n}\n\n/** base64(iv ‖ auth tag ‖ ciphertext) */\nexport function encryptSecret(key: Buffer, plaintext: string, aadValue: Buffer): string {\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n cipher.setAAD(aadValue);\n const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n return Buffer.concat([iv, cipher.getAuthTag(), data]).toString('base64');\n}\n\n/** Throws if the payload was tampered with or the AAD/key doesn't match. */\nexport function decryptSecret(key: Buffer, payload: string, aadValue: Buffer): string {\n const buf = Buffer.from(payload, 'base64');\n const iv = buf.subarray(0, IV_BYTES);\n const tag = buf.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);\n const data = buf.subarray(IV_BYTES + TAG_BYTES);\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAAD(aadValue);\n decipher.setAuthTag(tag);\n return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');\n}\n\nexport class DrizzleCredentialStore implements CredentialStore {\n private key: Buffer | undefined;\n\n constructor(\n private readonly db: DbExecutor,\n /** base64-encoded 32-byte key (CREDENTIAL_STORE_KEY). Validated on first use, not at boot. */\n private readonly keyBase64: string,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private getKey(): Buffer {\n if (!this.key) {\n const key = Buffer.from(this.keyBase64, 'base64');\n if (key.length !== 32) {\n throw new Error(\n 'CREDENTIAL_STORE_KEY must be a base64-encoded 32-byte key (generate with: openssl rand -base64 32)',\n );\n }\n this.key = key;\n }\n return this.key;\n }\n\n async store(orgId: string, secrets: Record<string, unknown>): Promise<string> {\n const id = genId('credential');\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, id));\n await this.db.insert(credentials).values({ orgId, id, ciphertext, keyVersion: KEY_VERSION });\n return id;\n }\n\n async reveal(orgId: string, ref: string): Promise<Record<string, unknown> | null> {\n const [row] = await this.db\n .select()\n .from(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)))\n .limit(1);\n if (!row) {\n return null;\n }\n const plaintext = decryptSecret(this.getKey(), row.ciphertext, aad(orgId, ref));\n return JSON.parse(plaintext) as Record<string, unknown>;\n }\n\n async update(orgId: string, ref: string, secrets: Record<string, unknown>): Promise<void> {\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, ref));\n await this.db\n .update(credentials)\n .set({ ciphertext, keyVersion: KEY_VERSION, updatedAt: new Date() })\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n\n async destroy(orgId: string, ref: string): Promise<void> {\n await this.db\n .delete(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n}\n"],"mappings":";;;;;;AAeA,MAAM,WAAW;;AAGjB,MAAM,cAAc;AAEpB,SAAS,IAAI,OAAe,IAAoB;CAC9C,OAAO,OAAO,KAAK,GAAG,MAAM,GAAG,MAAM,MAAM;AAC7C;;AAGA,SAAgB,cAAc,KAAa,WAAmB,UAA0B;CACtF,MAAM,KAAK,YAAY,QAAQ;CAC/B,MAAM,SAAS,eAAe,eAAe,KAAK,EAAE;CACpD,OAAO,OAAO,QAAQ;CACtB,MAAM,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;CAC7E,OAAO,OAAO,OAAO;EAAC;EAAI,OAAO,WAAW;EAAG;CAAI,CAAC,CAAC,CAAC,SAAS,QAAQ;AACzE;;AAGA,SAAgB,cAAc,KAAa,SAAiB,UAA0B;CACpF,MAAM,MAAM,OAAO,KAAK,SAAS,QAAQ;CACzC,MAAM,KAAK,IAAI,SAAS,GAAG,QAAQ;CACnC,MAAM,MAAM,IAAI,SAAS,UAAU,EAAoB;CACvD,MAAM,OAAO,IAAI,SAAS,EAAoB;CAC9C,MAAM,WAAW,iBAAiB,eAAe,KAAK,EAAE;CACxD,SAAS,OAAO,QAAQ;CACxB,SAAS,WAAW,GAAG;CACvB,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM;AACjF;AAEA,IAAa,yBAAb,MAA+D;CAI1C;CAEA;CACA;CANnB;CAEA,YACE,IAEA,WACA,SAAkC,YAClC;EAJiB,KAAA,KAAA;EAEA,KAAA,YAAA;EACA,KAAA,SAAA;CAChB;CAEH,SAAyB;EACvB,IAAI,CAAC,KAAK,KAAK;GACb,MAAM,MAAM,OAAO,KAAK,KAAK,WAAW,QAAQ;GAChD,IAAI,IAAI,WAAW,IACjB,MAAM,IAAI,MACR,oGACF;GAEF,KAAK,MAAM;EACb;EACA,OAAO,KAAK;CACd;CAEA,MAAM,MAAM,OAAe,SAAmD;EAC5E,MAAM,KAAK,MAAM,YAAY;EAC7B,MAAM,aAAa,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;EACvF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAAO;GAAE;GAAO;GAAI;GAAY,YAAY;EAAY,CAAC;EAC3F,OAAO;CACT;CAEA,MAAM,OAAO,OAAe,KAAsD;EAChF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC,CAAC,CACjE,MAAM,CAAC;EACV,IAAI,CAAC,KACH,OAAO;EAET,MAAM,YAAY,cAAc,KAAK,OAAO,GAAG,IAAI,YAAY,IAAI,OAAO,GAAG,CAAC;EAC9E,OAAO,KAAK,MAAM,SAAS;CAC7B;CAEA,MAAM,OAAO,OAAe,KAAa,SAAiD;EACxF,MAAM,aAAa,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,OAAO,GAAG,IAAI,OAAO,GAAG,CAAC;EACxF,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GAAE;GAAY,YAAY;GAAa,2BAAW,IAAI,KAAK;EAAE,CAAC,CAAC,CACnE,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC;CACrE;CAEA,MAAM,QAAQ,OAAe,KAA4B;EACvD,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC;CACrE;AACF"}
1
+ {"version":3,"file":"credentials.js","names":[],"sources":["../../src/repositories/credentials.ts"],"sourcesContent":["// credentials — Drizzle-backed secure credential store (implements the shared\n// core port). Secrets are JSON documents: serialized here, encrypted with\n// AES-256-GCM via node:crypto (single 32-byte key from the environment), and\n// parsed back to objects on reveal — callers never (de)serialize. The AAD\n// binds each ciphertext to its (org, id) row, so moving a ciphertext to\n// another row or org makes it undecryptable. Plaintext exists only inside\n// store/reveal — it is never logged, listed, or returned elsewhere.\nimport { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type { CredentialStore, Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { credentials } from \"../schema/credentials.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\n/** Version of the key currently used for new writes (stored per row for rotation). */\nconst KEY_VERSION = 1;\n\nfunction aad(orgId: string, id: string): Buffer {\n return Buffer.from(`${orgId}:${id}`, \"utf8\");\n}\n\n/** base64(iv ‖ auth tag ‖ ciphertext) */\nexport function encryptSecret(key: Buffer, plaintext: string, aadValue: Buffer): string {\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n cipher.setAAD(aadValue);\n const data = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n return Buffer.concat([iv, cipher.getAuthTag(), data]).toString(\"base64\");\n}\n\n/** Throws if the payload was tampered with or the AAD/key doesn't match. */\nexport function decryptSecret(key: Buffer, payload: string, aadValue: Buffer): string {\n const buf = Buffer.from(payload, \"base64\");\n const iv = buf.subarray(0, IV_BYTES);\n const tag = buf.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);\n const data = buf.subarray(IV_BYTES + TAG_BYTES);\n const decipher = createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAAD(aadValue);\n decipher.setAuthTag(tag);\n return Buffer.concat([decipher.update(data), decipher.final()]).toString(\"utf8\");\n}\n\nexport class DrizzleCredentialStore implements CredentialStore {\n private key: Buffer | undefined;\n\n constructor(\n private readonly db: DbExecutor,\n /** base64-encoded 32-byte key (CREDENTIAL_STORE_KEY). Validated on first use, not at boot. */\n private readonly keyBase64: string,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private getKey(): Buffer {\n if (!this.key) {\n const key = Buffer.from(this.keyBase64, \"base64\");\n if (key.length !== 32) {\n throw new Error(\n \"CREDENTIAL_STORE_KEY must be a base64-encoded 32-byte key (generate with: openssl rand -base64 32)\",\n );\n }\n this.key = key;\n }\n return this.key;\n }\n\n async store(orgId: string, secrets: Record<string, unknown>): Promise<string> {\n const id = genId(\"credential\");\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, id));\n await this.db.insert(credentials).values({ orgId, id, ciphertext, keyVersion: KEY_VERSION });\n return id;\n }\n\n async reveal(orgId: string, ref: string): Promise<Record<string, unknown> | null> {\n const [row] = await this.db\n .select()\n .from(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)))\n .limit(1);\n if (!row) {\n return null;\n }\n const plaintext = decryptSecret(this.getKey(), row.ciphertext, aad(orgId, ref));\n return JSON.parse(plaintext) as Record<string, unknown>;\n }\n\n async update(orgId: string, ref: string, secrets: Record<string, unknown>): Promise<void> {\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, ref));\n await this.db\n .update(credentials)\n .set({ ciphertext, keyVersion: KEY_VERSION, updatedAt: new Date() })\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n\n async destroy(orgId: string, ref: string): Promise<void> {\n await this.db\n .delete(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n}\ntranslateDbErrors(DrizzleCredentialStore);\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Comment, CommentReport, ListCommentsQuery, Logger, Page, ReactionEmoji } from "@headless-lms/core/types";
3
- import { AuthorRecord, CommentReactions, CommentWithContext, DiscussionRepository } from "@headless-lms/core/discussion";
2
+ import { Comment, CommentReport, FlaggedComment, ListCommentsQuery, Logger, Page, ReactionEmoji } from "@headless-lms/core/types";
3
+ import { AuthorRecord, CommentReactions, CommentWithContext, DiscussionRepository, PostThreadsQuery } from "@headless-lms/core/discussion";
4
4
  //#region src/repositories/discussion.d.ts
5
5
  declare class DrizzleDiscussionRepository implements DiscussionRepository {
6
6
  private readonly db;
@@ -8,14 +8,21 @@ declare class DrizzleDiscussionRepository implements DiscussionRepository {
8
8
  constructor(db: NodePgDatabase, logger?: Logger);
9
9
  insertComment(orgId: string, comment: Comment): Promise<Comment>;
10
10
  findComment(orgId: string, id: string): Promise<Comment | null>;
11
- updateComment(orgId: string, id: string, patch: Partial<Pick<Comment, 'body' | 'status' | 'removedBy' | 'updatedAt'>>): Promise<Comment | null>;
11
+ updateComment(orgId: string, id: string, patch: Partial<Pick<Comment, "body" | "status" | "removedBy" | "updatedAt">>): Promise<Comment | null>;
12
12
  listByActivity(orgId: string, activityId: string): Promise<Comment[]>;
13
+ listThreadsByPost(orgId: string, postId: string, query: PostThreadsQuery): Promise<Page<Comment>>;
13
14
  reactionsOf(orgId: string, commentIds: string[], viewerId: string): Promise<Record<string, CommentReactions>>;
14
15
  setReaction(orgId: string, commentId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
15
16
  authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;
16
17
  insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null>;
18
+ listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;
19
+ publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>>;
17
20
  listOpenReports(orgId: string, commentIds: string[]): Promise<CommentReport[]>;
18
- resolveReportsFor(orgId: string, commentId: string, resolvedAt: Date): Promise<void>;
21
+ resolveReportsFor(orgId: string, commentId: string, resolved: {
22
+ resolvedAt: Date;
23
+ resolvedBy: string;
24
+ resolution: "dismissed" | "removed";
25
+ }): Promise<void>;
19
26
  /** The list's one join: comments to their activity, which carries both the
20
27
  * course (denormalised, so the module never enters the query) and the title.
21
28
  * The course filter and the row's activity title come out of that same pass,
@@ -1,11 +1,12 @@
1
- import { users } from "../schema/identity.js";
2
- import { orgUsers } from "../schema/organizations.js";
3
1
  import { activities } from "../schema/content.js";
4
- import { commentReactions, commentReports, comments } from "../schema/discussion.js";
2
+ import { commentFlags, commentReactions, comments, posts } from "../schema/discussion.js";
3
+ import { userProfiles, users } from "../schema/identity.js";
4
+ import { orgUsers } from "../schema/organizations.js";
5
5
  import "../schema/index.js";
6
+ import { translateDbErrors } from "./pg-errors.js";
6
7
  import { user } from "../schema/better-auth.js";
7
- import { orgUserProfileColumns } from "./org-user-profile.js";
8
- import { and, asc, desc, eq, ilike, inArray, isNull, sql } from "drizzle-orm";
8
+ import { authorProfileColumns } from "./org-user-profile.js";
9
+ import { and, asc, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm";
9
10
  import { noopLogger } from "@headless-lms/core/shared/logger";
10
11
  //#region src/repositories/discussion.ts
11
12
  /** The title lives in the activity's opaque settings blob; the moderation card
@@ -24,6 +25,8 @@ function toComment(row) {
24
25
  id: row.id,
25
26
  orgId: row.orgId,
26
27
  activityId: row.activityId,
28
+ postId: row.postId,
29
+ communityId: row.communityId,
27
30
  parentId: row.parentId ?? null,
28
31
  orgUserId: row.orgUserId,
29
32
  body: row.body,
@@ -45,6 +48,8 @@ var DrizzleDiscussionRepository = class {
45
48
  id: comment.id,
46
49
  orgId,
47
50
  activityId: comment.activityId,
51
+ postId: comment.postId,
52
+ communityId: comment.communityId,
48
53
  parentId: comment.parentId,
49
54
  orgUserId: comment.orgUserId,
50
55
  body: comment.body,
@@ -70,6 +75,25 @@ var DrizzleDiscussionRepository = class {
70
75
  async listByActivity(orgId, activityId) {
71
76
  return (await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), eq(comments.activityId, activityId))).orderBy(comments.createdAt)).map(toComment);
72
77
  }
78
+ async listThreadsByPost(orgId, postId, query) {
79
+ const rootReadable = query.seesHeld ? void 0 : or(ne(comments.status, "pending"), eq(comments.orgUserId, query.viewerId));
80
+ const replyReadable = query.seesHeld ? sql`` : sql` and (rep.status = 'published' or rep.org_user_id = ${query.viewerId})`;
81
+ const where = and(eq(comments.orgId, orgId), eq(comments.postId, postId), isNull(comments.parentId), rootReadable, or(ne(comments.status, "removed"), sql`exists (
82
+ select 1 from comments rep
83
+ where rep.org_id = ${comments.orgId}
84
+ and rep.parent_id = ${comments.id}
85
+ and rep.status <> 'removed'${replyReadable}
86
+ )`));
87
+ const roots = await this.db.select().from(comments).where(where).orderBy(asc(comments.createdAt), asc(comments.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
88
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(comments).where(where);
89
+ const replies = roots.length === 0 ? [] : await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), inArray(comments.parentId, roots.map((root) => root.id)))).orderBy(asc(comments.createdAt), asc(comments.id));
90
+ return {
91
+ rows: [...roots, ...replies].map(toComment),
92
+ total,
93
+ page: query.page,
94
+ pageSize: query.pageSize
95
+ };
96
+ }
73
97
  async reactionsOf(orgId, commentIds, viewerId) {
74
98
  if (commentIds.length === 0) return {};
75
99
  const rows = await this.db.select({
@@ -107,10 +131,7 @@ var DrizzleDiscussionRepository = class {
107
131
  }
108
132
  async authorsOf(orgId, orgUserIds) {
109
133
  if (orgUserIds.length === 0) return {};
110
- const rows = await this.db.select({
111
- ...orgUserProfileColumns,
112
- role: orgUsers.role
113
- }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
134
+ const rows = await this.db.select(authorProfileColumns).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
114
135
  const byId = {};
115
136
  for (const row of rows) byId[row.id] = {
116
137
  id: row.id,
@@ -118,28 +139,63 @@ var DrizzleDiscussionRepository = class {
118
139
  lastName: row.lastName,
119
140
  email: row.email,
120
141
  image: row.image ?? null,
121
- role: row.role
142
+ role: row.role,
143
+ displayName: row.displayName ?? null,
144
+ headline: row.headline ?? null,
145
+ imageAssetOrgId: row.imageAssetOrgId ?? null,
146
+ imageAssetId: row.imageAssetId ?? null
122
147
  };
123
148
  return byId;
124
149
  }
125
150
  async insertReport(orgId, report) {
126
- const [row] = await this.db.insert(commentReports).values({
151
+ const [row] = await this.db.insert(commentFlags).values({
127
152
  id: report.id,
128
153
  orgId,
129
154
  commentId: report.commentId,
130
155
  orgUserId: report.orgUserId,
131
156
  reason: report.reason,
132
157
  resolvedAt: report.resolvedAt ? new Date(report.resolvedAt) : null,
158
+ resolution: report.resolution,
159
+ resolvedBy: report.resolvedBy,
133
160
  createdAt: new Date(report.createdAt)
134
161
  }).onConflictDoNothing().returning();
135
162
  return row ?? null;
136
163
  }
164
+ async listOpenFlagsForSpace(orgId, spaceId, limit) {
165
+ return (await this.db.select({
166
+ flag: commentFlags,
167
+ comment: {
168
+ id: comments.id,
169
+ postId: posts.id,
170
+ body: comments.body,
171
+ orgUserId: comments.orgUserId,
172
+ status: comments.status
173
+ }
174
+ }).from(commentFlags).innerJoin(comments, and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId))).innerJoin(posts, and(eq(posts.orgId, comments.orgId), eq(posts.id, comments.postId))).where(and(eq(commentFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(commentFlags.resolvedAt))).orderBy(asc(commentFlags.createdAt), asc(commentFlags.id)).limit(limit)).map((row) => ({
175
+ ...row.flag,
176
+ comment: row.comment
177
+ }));
178
+ }
179
+ async publishedCommentCounts(orgId, postIds) {
180
+ if (postIds.length === 0) return {};
181
+ const rows = await this.db.select({
182
+ postId: comments.postId,
183
+ count: sql`cast(count(*) as int)`
184
+ }).from(comments).where(and(eq(comments.orgId, orgId), inArray(comments.postId, postIds), eq(comments.status, "published"))).groupBy(comments.postId);
185
+ const byPost = {};
186
+ for (const row of rows) if (row.postId !== null) byPost[row.postId] = row.count;
187
+ return byPost;
188
+ }
137
189
  async listOpenReports(orgId, commentIds) {
138
190
  if (commentIds.length === 0) return [];
139
- return await this.db.select().from(commentReports).where(and(eq(commentReports.orgId, orgId), inArray(commentReports.commentId, commentIds), isNull(commentReports.resolvedAt))).orderBy(commentReports.createdAt);
191
+ return await this.db.select().from(commentFlags).where(and(eq(commentFlags.orgId, orgId), inArray(commentFlags.commentId, commentIds), isNull(commentFlags.resolvedAt))).orderBy(commentFlags.createdAt);
140
192
  }
141
- async resolveReportsFor(orgId, commentId, resolvedAt) {
142
- await this.db.update(commentReports).set({ resolvedAt }).where(and(eq(commentReports.orgId, orgId), eq(commentReports.commentId, commentId), isNull(commentReports.resolvedAt)));
193
+ async resolveReportsFor(orgId, commentId, resolved) {
194
+ await this.db.update(commentFlags).set({
195
+ resolvedAt: resolved.resolvedAt,
196
+ resolvedBy: resolved.resolvedBy,
197
+ resolution: resolved.resolution
198
+ }).where(and(eq(commentFlags.orgId, orgId), eq(commentFlags.commentId, commentId), isNull(commentFlags.resolvedAt)));
143
199
  }
144
200
  /** The list's one join: comments to their activity, which carries both the
145
201
  * course (denormalised, so the module never enters the query) and the title.
@@ -165,7 +221,7 @@ var DrizzleDiscussionRepository = class {
165
221
  if (query.orgUserId) conditions.push(eq(comments.orgUserId, query.orgUserId));
166
222
  if (query.reported !== void 0) {
167
223
  const hasOpenReport = sql`exists (
168
- select 1 from ${commentReports} r
224
+ select 1 from ${commentFlags} r
169
225
  where r.org_id = ${comments.orgId}
170
226
  and r.comment_id = ${comments.id}
171
227
  and r.resolved_at is null
@@ -195,6 +251,7 @@ var DrizzleDiscussionRepository = class {
195
251
  };
196
252
  }
197
253
  };
254
+ translateDbErrors(DrizzleDiscussionRepository);
198
255
  //#endregion
199
256
  export { DrizzleDiscussionRepository };
200
257