@headless-lms/adapter-db 0.1.0 → 0.2.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 (98) 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 +2 -1
  5. package/dist/index.js +3 -2
  6. package/dist/migrate.js.map +1 -1
  7. package/dist/repositories/assets.d.ts +1 -1
  8. package/dist/repositories/assets.js +4 -1
  9. package/dist/repositories/assets.js.map +1 -1
  10. package/dist/repositories/automations.d.ts +7 -5
  11. package/dist/repositories/automations.js +35 -10
  12. package/dist/repositories/automations.js.map +1 -1
  13. package/dist/repositories/content.d.ts +21 -7
  14. package/dist/repositories/content.js +168 -18
  15. package/dist/repositories/content.js.map +1 -1
  16. package/dist/repositories/course-analytics.d.ts +15 -0
  17. package/dist/repositories/course-analytics.js +137 -0
  18. package/dist/repositories/course-analytics.js.map +1 -0
  19. package/dist/repositories/credentials.js +2 -0
  20. package/dist/repositories/credentials.js.map +1 -1
  21. package/dist/repositories/dashboard.js +2 -0
  22. package/dist/repositories/dashboard.js.map +1 -1
  23. package/dist/repositories/discussion.d.ts +1 -1
  24. package/dist/repositories/discussion.js +2 -0
  25. package/dist/repositories/discussion.js.map +1 -1
  26. package/dist/repositories/entitlements.d.ts +11 -5
  27. package/dist/repositories/entitlements.js +42 -17
  28. package/dist/repositories/entitlements.js.map +1 -1
  29. package/dist/repositories/identity.js +2 -0
  30. package/dist/repositories/identity.js.map +1 -1
  31. package/dist/repositories/integrations.d.ts +1 -1
  32. package/dist/repositories/integrations.js +2 -0
  33. package/dist/repositories/integrations.js.map +1 -1
  34. package/dist/repositories/learn.js +6 -4
  35. package/dist/repositories/learn.js.map +1 -1
  36. package/dist/repositories/members.js +2 -0
  37. package/dist/repositories/members.js.map +1 -1
  38. package/dist/repositories/org-user-profile.js.map +1 -1
  39. package/dist/repositories/organizations.d.ts +4 -6
  40. package/dist/repositories/organizations.js +26 -45
  41. package/dist/repositories/organizations.js.map +1 -1
  42. package/dist/repositories/outbox.js +3 -0
  43. package/dist/repositories/outbox.js.map +1 -1
  44. package/dist/repositories/pg-errors.d.ts +53 -5
  45. package/dist/repositories/pg-errors.js +97 -8
  46. package/dist/repositories/pg-errors.js.map +1 -1
  47. package/dist/repositories/progress.d.ts +1 -1
  48. package/dist/repositories/progress.js +3 -0
  49. package/dist/repositories/progress.js.map +1 -1
  50. package/dist/repositories/settings.js +2 -0
  51. package/dist/repositories/settings.js.map +1 -1
  52. package/dist/repositories/students.d.ts +2 -1
  53. package/dist/repositories/students.js +80 -3
  54. package/dist/repositories/students.js.map +1 -1
  55. package/dist/schema/assets.d.ts +17 -0
  56. package/dist/schema/assets.js +2 -1
  57. package/dist/schema/assets.js.map +1 -1
  58. package/dist/schema/automations.d.ts +85 -8
  59. package/dist/schema/automations.js +7 -4
  60. package/dist/schema/automations.js.map +1 -1
  61. package/dist/schema/better-auth.js.map +1 -1
  62. package/dist/schema/content.d.ts +272 -22
  63. package/dist/schema/content.js +40 -5
  64. package/dist/schema/content.js.map +1 -1
  65. package/dist/schema/credentials.js +1 -1
  66. package/dist/schema/credentials.js.map +1 -1
  67. package/dist/schema/discussion.d.ts +34 -0
  68. package/dist/schema/discussion.js +4 -2
  69. package/dist/schema/discussion.js.map +1 -1
  70. package/dist/schema/entitlements.d.ts +52 -1
  71. package/dist/schema/entitlements.js +15 -5
  72. package/dist/schema/entitlements.js.map +1 -1
  73. package/dist/schema/identity.js.map +1 -1
  74. package/dist/schema/index.d.ts +3 -3
  75. package/dist/schema/index.js +4 -2
  76. package/dist/schema/index.js.map +1 -1
  77. package/dist/schema/integrations.js +1 -1
  78. package/dist/schema/integrations.js.map +1 -1
  79. package/dist/schema/organizations.d.ts +17 -0
  80. package/dist/schema/organizations.js +2 -1
  81. package/dist/schema/organizations.js.map +1 -1
  82. package/dist/schema/outbox.d.ts +17 -0
  83. package/dist/schema/outbox.js +1 -0
  84. package/dist/schema/outbox.js.map +1 -1
  85. package/dist/schema/progress.d.ts +17 -0
  86. package/dist/schema/progress.js +1 -0
  87. package/dist/schema/progress.js.map +1 -1
  88. package/dist/schema/settings.js.map +1 -1
  89. package/dist/unit-of-work.js +7 -2
  90. package/dist/unit-of-work.js.map +1 -1
  91. package/drizzle/0000_baseline.sql +0 -1
  92. package/drizzle/0001_new_sentry.sql +43 -0
  93. package/drizzle/0002_automation_kinds.sql +7 -0
  94. package/drizzle/meta/0000_snapshot.json +167 -597
  95. package/drizzle/meta/0001_snapshot.json +3769 -0
  96. package/drizzle/meta/0002_snapshot.json +3770 -0
  97. package/drizzle/meta/_journal.json +16 -2
  98. package/package.json +3 -2
@@ -1,14 +1,23 @@
1
- import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
2
- import { isUniqueViolation } from "./pg-errors.js";
1
+ import { activities, activityAssets, bundleItems, bundles, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
2
+ import { settings } from "../schema/settings.js";
3
+ import { isUniqueViolation, translateDbErrors } from "./pg-errors.js";
4
+ import { deepMerge } from "./settings.js";
3
5
  import { and, asc, count, desc, eq, ilike, inArray, or, sql } from "drizzle-orm";
4
6
  import { genId } from "@headless-lms/core/shared/id";
5
- import { noopLogger } from "@headless-lms/core/shared/logger";
6
7
  import { ConflictError, NotFoundError } from "@headless-lms/core/shared/errors";
8
+ import { noopLogger } from "@headless-lms/core/shared/logger";
9
+ import { courseSettingsDefaults } from "@headless-lms/core/schemas";
7
10
  //#region src/repositories/content.ts
8
- function toCourse(row) {
11
+ const SETTINGS_NAMESPACE = "content";
12
+ const courseSettingsJoin = and(eq(settings.orgId, courses.orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, courses.id));
13
+ function toCourse(row, storedSettings) {
9
14
  return {
10
15
  ...row,
11
- status: row.status
16
+ status: row.status,
17
+ settings: {
18
+ ...courseSettingsDefaults,
19
+ ...storedSettings
20
+ }
12
21
  };
13
22
  }
14
23
  const sortColumns = {
@@ -33,6 +42,11 @@ function toDownload(row) {
33
42
  status: row.status
34
43
  };
35
44
  }
45
+ const bundleSortColumns = {
46
+ name: bundles.name,
47
+ createdAt: bundles.createdAt,
48
+ updatedAt: bundles.updatedAt
49
+ };
36
50
  var DrizzleContentRepository = class {
37
51
  db;
38
52
  logger;
@@ -45,7 +59,7 @@ var DrizzleContentRepository = class {
45
59
  tx(fn) {
46
60
  return this.db.transaction(fn);
47
61
  }
48
- async list(orgId, query) {
62
+ async listCourses(orgId, query) {
49
63
  const conditions = [eq(courses.orgId, orgId)];
50
64
  if (query.status) conditions.push(eq(courses.status, query.status));
51
65
  if (query.category) conditions.push(eq(courses.category, query.category));
@@ -68,20 +82,26 @@ var DrizzleContentRepository = class {
68
82
  }
69
83
  const sortExpr = sortColumns[sortKey] ?? courses.createdAt;
70
84
  const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
71
- const rows = await this.db.select().from(courses).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
85
+ const rows = await this.db.select({
86
+ course: courses,
87
+ settingsValue: settings.value
88
+ }).from(courses).leftJoin(settings, courseSettingsJoin).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
72
89
  const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);
73
90
  return {
74
- rows: rows.map(toCourse),
91
+ rows: rows.map((row) => toCourse(row.course, row.settingsValue)),
75
92
  total: totalRow?.value ?? 0,
76
93
  page: query.page,
77
94
  pageSize: query.pageSize
78
95
  };
79
96
  }
80
- async findById(orgId, id) {
81
- const [row] = await this.db.select().from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).limit(1);
82
- return row ? toCourse(row) : null;
97
+ async findCourseById(orgId, id) {
98
+ const [row] = await this.db.select({
99
+ course: courses,
100
+ settingsValue: settings.value
101
+ }).from(courses).leftJoin(settings, courseSettingsJoin).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).limit(1);
102
+ return row ? toCourse(row.course, row.settingsValue) : null;
83
103
  }
84
- async create(orgId, input, slug) {
104
+ async createCourse(orgId, input, slug) {
85
105
  const id = genId("course");
86
106
  await this.db.insert(contentItems).values({
87
107
  orgId,
@@ -97,31 +117,68 @@ var DrizzleContentRepository = class {
97
117
  category: input.category ?? ""
98
118
  }).returning({ id: courses.id });
99
119
  if (!inserted) throw new Error("failed to insert course");
100
- const created = await this.findById(orgId, inserted.id);
120
+ const created = await this.findCourseById(orgId, inserted.id);
101
121
  if (!created) throw new Error("failed to load created course");
102
122
  return created;
103
123
  }
104
- async update(orgId, id, patch) {
124
+ async updateCourse(orgId, id, patch) {
105
125
  const set = { updatedAt: /* @__PURE__ */ new Date() };
106
126
  if (patch.title !== void 0) set.title = patch.title;
107
127
  if (patch.description !== void 0) set.description = patch.description;
108
128
  if (patch.category !== void 0) set.category = patch.category;
109
129
  if (patch.status !== void 0) set.status = patch.status;
110
130
  if (patch.thumbnailAssetId !== void 0) set.thumbnailAssetId = patch.thumbnailAssetId;
111
- if (patch.settings !== void 0) set.settings = sql`${courses.settings} || ${JSON.stringify(patch.settings)}::jsonb`;
112
131
  const [updated] = await this.db.update(courses).set(set).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).returning({ id: courses.id });
113
132
  if (!updated) return null;
114
- return this.findById(orgId, id);
133
+ return this.findCourseById(orgId, id);
134
+ }
135
+ async patchCourseSettings(orgId, id, value) {
136
+ const key = and(eq(settings.orgId, orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, id));
137
+ const merged = await this.tx(async (tx) => {
138
+ let [existing] = await tx.select().from(settings).where(key).for("update");
139
+ if (!existing) {
140
+ const [inserted] = await tx.insert(settings).values({
141
+ orgId,
142
+ namespace: SETTINGS_NAMESPACE,
143
+ scopeId: id,
144
+ value
145
+ }).onConflictDoNothing().returning();
146
+ if (inserted) return inserted.value;
147
+ [existing] = await tx.select().from(settings).where(key).for("update");
148
+ if (!existing) throw new Error("course settings patch: row vanished mid-transaction");
149
+ }
150
+ const [updated] = await tx.update(settings).set({
151
+ value: deepMerge(existing.value, value),
152
+ updatedAt: /* @__PURE__ */ new Date()
153
+ }).where(key).returning();
154
+ if (!updated) throw new Error("course settings patch returned no row");
155
+ return updated.value;
156
+ });
157
+ return {
158
+ ...courseSettingsDefaults,
159
+ ...merged
160
+ };
115
161
  }
116
- async delete(orgId, id) {
162
+ async deleteCourse(orgId, id) {
117
163
  return (await this.db.delete(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, "course"))).returning({ id: contentItems.id })).length > 0;
118
164
  }
119
- listForCourse(orgId, courseId) {
165
+ listCourseModules(orgId, courseId) {
120
166
  return this.tx((tx) => this.listModules(tx, orgId, courseId));
121
167
  }
122
168
  async listActivitiesForCourse(orgId, courseId) {
123
169
  return this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activities.moduleId, activities.seq);
124
170
  }
171
+ async listActivityAssetsForCourse(orgId, courseId) {
172
+ return this.db.select({
173
+ orgId: activityAssets.orgId,
174
+ id: activityAssets.id,
175
+ activityId: activityAssets.activityId,
176
+ assetId: activityAssets.assetId,
177
+ seq: activityAssets.seq,
178
+ createdAt: activityAssets.createdAt,
179
+ updatedAt: activityAssets.updatedAt
180
+ }).from(activityAssets).innerJoin(activities, and(eq(activities.orgId, activityAssets.orgId), eq(activities.id, activityAssets.activityId))).where(and(eq(activityAssets.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activityAssets.activityId, activityAssets.seq);
181
+ }
125
182
  async findActivity(orgId, activityId) {
126
183
  const [row] = await this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId))).limit(1);
127
184
  return row ?? null;
@@ -343,7 +400,100 @@ var DrizzleContentRepository = class {
343
400
  for (const [seq, assetId] of assetIds.entries()) await this.db.update(downloadAssets).set({ seq }).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId), eq(downloadAssets.assetId, assetId)));
344
401
  return this.listDownloadAssets(orgId, downloadId);
345
402
  }
403
+ async listBundles(orgId, query) {
404
+ const conditions = [eq(bundles.orgId, orgId)];
405
+ const search = query.search?.trim();
406
+ if (search) conditions.push(ilike(bundles.name, `%${search}%`));
407
+ const where = and(...conditions);
408
+ let sortKey = "createdAt";
409
+ let direction = "desc";
410
+ if (query.sort) {
411
+ const isDesc = query.sort.startsWith("-");
412
+ const key = isDesc ? query.sort.slice(1) : query.sort;
413
+ if (key in bundleSortColumns) {
414
+ sortKey = key;
415
+ direction = isDesc ? "desc" : "asc";
416
+ }
417
+ }
418
+ const sortExpr = bundleSortColumns[sortKey] ?? bundles.createdAt;
419
+ const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
420
+ const rows = await this.db.select().from(bundles).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
421
+ const [totalRow] = await this.db.select({ value: count() }).from(bundles).where(where);
422
+ return {
423
+ rows,
424
+ total: totalRow?.value ?? 0,
425
+ page: query.page,
426
+ pageSize: query.pageSize
427
+ };
428
+ }
429
+ async findBundleById(orgId, id) {
430
+ const [row] = await this.db.select().from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).limit(1);
431
+ return row ?? null;
432
+ }
433
+ /** The bundle and its initial items are one write: an unknown content id
434
+ * rolls the bundle back rather than leaving it created and half-filled. */
435
+ async createBundle(orgId, input) {
436
+ const contentIds = [...new Set(input.contentIds ?? [])];
437
+ return this.tx(async (tx) => {
438
+ const [row] = await tx.insert(bundles).values({
439
+ orgId,
440
+ id: genId("bundle"),
441
+ name: input.name
442
+ }).returning();
443
+ if (!row) throw new Error("failed to load created bundle");
444
+ if (contentIds.length > 0) {
445
+ const found = await tx.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), inArray(contentItems.id, contentIds)));
446
+ const known = new Set(found.map((item) => item.id));
447
+ const missing = contentIds.find((id) => !known.has(id));
448
+ if (missing) throw new NotFoundError("Content", missing);
449
+ await tx.insert(bundleItems).values(contentIds.map((contentId) => ({
450
+ orgId,
451
+ bundleId: row.id,
452
+ contentId
453
+ })));
454
+ }
455
+ return row;
456
+ });
457
+ }
458
+ async updateBundle(orgId, id, patch) {
459
+ const set = { updatedAt: /* @__PURE__ */ new Date() };
460
+ if (patch.name !== void 0) set.name = patch.name;
461
+ const [row] = await this.db.update(bundles).set(set).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning();
462
+ return row ?? null;
463
+ }
464
+ async deleteBundle(orgId, id) {
465
+ return (await this.db.delete(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning({ id: bundles.id })).length > 0;
466
+ }
467
+ async listBundleItems(orgId, bundleId) {
468
+ return this.db.select().from(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId))).orderBy(asc(bundleItems.createdAt));
469
+ }
470
+ /** Assert the bundle exists in the org; throw otherwise. */
471
+ async assertBundle(orgId, bundleId) {
472
+ const [row] = await this.db.select({ id: bundles.id }).from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, bundleId))).limit(1);
473
+ if (!row) throw new NotFoundError("Bundle", bundleId);
474
+ }
475
+ async addBundleItem(orgId, bundleId, contentId) {
476
+ await this.assertBundle(orgId, bundleId);
477
+ const [item] = await this.db.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
478
+ if (!item) throw new NotFoundError("Content", contentId);
479
+ await this.db.insert(bundleItems).values({
480
+ orgId,
481
+ bundleId,
482
+ contentId
483
+ }).onConflictDoNothing({ target: [
484
+ bundleItems.orgId,
485
+ bundleItems.bundleId,
486
+ bundleItems.contentId
487
+ ] });
488
+ return this.listBundleItems(orgId, bundleId);
489
+ }
490
+ async removeBundleItem(orgId, bundleId, contentId) {
491
+ await this.assertBundle(orgId, bundleId);
492
+ await this.db.delete(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId), eq(bundleItems.contentId, contentId)));
493
+ return this.listBundleItems(orgId, bundleId);
494
+ }
346
495
  };
496
+ translateDbErrors(DrizzleContentRepository);
347
497
  //#endregion
348
498
  export { DrizzleContentRepository };
349
499
 
@@ -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 ContentRepository,\n Course,\n CourseSettings,\n CourseStatus,\n CreateBundleInput,\n CreateCourseInput,\n CreateDownloadInput,\n Download,\n DownloadAsset,\n DownloadStatus,\n ListBundlesQuery,\n ListCoursesQuery,\n ListDownloadsQuery,\n Module,\n Page,\n SaveActivityInput,\n UpdateBundleInput,\n UpdateCourseInput,\n UpdateDownloadInput,\n} from \"@headless-lms/core/content\";\nimport { courseSettingsDefaults, type Setting } from \"@headless-lms/core/schemas\";\nimport {\n bundles,\n bundleItems,\n contentItems,\n courses,\n modules,\n activities,\n activityAssets,\n downloads,\n downloadAssets,\n} from \"../schema/content.js\";\nimport { settings } from \"../schema/settings.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, translateDbErrors } from \"./pg-errors.js\";\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\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 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(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}\ntranslateDbErrors(DrizzleContentRepository);\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger } from "@headless-lms/core/shared/ports";
3
+ import { CourseAnalytics, CourseEnrollmentPoint, CoursesReportRepository } from "@headless-lms/core/reporting/courses";
4
+ //#region src/repositories/course-analytics.d.ts
5
+ declare class DrizzleCourseAnalyticsRepository implements CoursesReportRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ private courseExists;
10
+ analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
11
+ enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
12
+ }
13
+ //#endregion
14
+ export { DrizzleCourseAnalyticsRepository };
15
+ //# sourceMappingURL=course-analytics.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { courses } from "../schema/content.js";
2
+ import { entitlements } from "../schema/entitlements.js";
3
+ import "../schema/index.js";
4
+ import { translateDbErrors } from "./pg-errors.js";
5
+ import { and, eq, sql } from "drizzle-orm";
6
+ import { noopLogger } from "@headless-lms/core/shared/logger";
7
+ //#region src/repositories/course-analytics.ts
8
+ var DrizzleCourseAnalyticsRepository = class {
9
+ db;
10
+ logger;
11
+ constructor(db, logger = noopLogger) {
12
+ this.db = db;
13
+ this.logger = logger;
14
+ }
15
+ async courseExists(orgId, courseId) {
16
+ const [row] = await this.db.select({ id: courses.id }).from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));
17
+ return row !== void 0;
18
+ }
19
+ async analytics(orgId, courseId) {
20
+ if (!await this.courseExists(orgId, courseId)) return null;
21
+ const cohort = sql`
22
+ select org_user_id from entitlements
23
+ where org_id = ${orgId}
24
+ and content_id = ${courseId}
25
+ and status = 'active'
26
+ and (expires_at is null or expires_at >= now())
27
+ `;
28
+ const publishedActivities = sql`
29
+ select a.id, a.settings, a.seq, a.module_id
30
+ from activities a
31
+ where a.org_id = ${orgId}
32
+ and a.course_id = ${courseId}
33
+ and coalesce((a.settings ->> 'published')::boolean, true)
34
+ `;
35
+ const summary = (await this.db.execute(sql`
36
+ with cohort as (${cohort}),
37
+ acts as (${publishedActivities}),
38
+ per_learner as (
39
+ select c.org_user_id,
40
+ count(pr.id) filter (where pr.completed_at is not null) as done
41
+ from cohort c
42
+ left join progress_records pr
43
+ on pr.org_id = ${orgId}
44
+ and pr.org_user_id = c.org_user_id
45
+ and pr.target_type = 'activity'
46
+ and pr.target_id in (select id from acts)
47
+ group by c.org_user_id
48
+ )
49
+ select
50
+ (select count(*) from cohort)::int as enrolled,
51
+ (
52
+ select count(distinct pr.org_user_id) from progress_records pr
53
+ where pr.org_id = ${orgId}
54
+ and pr.org_user_id in (select org_user_id from cohort)
55
+ and (
56
+ (pr.target_type = 'activity' and pr.target_id in (select id from acts))
57
+ or (pr.target_type = 'course' and pr.target_id = ${courseId})
58
+ )
59
+ )::int as started,
60
+ (
61
+ select count(*) from progress_records pr
62
+ where pr.org_id = ${orgId}
63
+ and pr.org_user_id in (select org_user_id from cohort)
64
+ and pr.target_type = 'course'
65
+ and pr.target_id = ${courseId}
66
+ and pr.completed_at is not null
67
+ )::int as completed,
68
+ coalesce((
69
+ select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))
70
+ from per_learner
71
+ ), 0)::int as avg_progress
72
+ `)).rows[0];
73
+ const activities = (await this.db.execute(sql`
74
+ with cohort as (${cohort})
75
+ select
76
+ a.id,
77
+ coalesce(a.settings ->> 'title', '') as title,
78
+ m.id as module_id,
79
+ m.title as module_title,
80
+ count(pr.id)::int as started,
81
+ count(pr.id) filter (where pr.completed_at is not null)::int as completed
82
+ from activities a
83
+ join modules m on m.org_id = a.org_id and m.id = a.module_id
84
+ left join progress_records pr
85
+ on pr.org_id = ${orgId}
86
+ and pr.target_type = 'activity'
87
+ and pr.target_id = a.id
88
+ and pr.org_user_id in (select org_user_id from cohort)
89
+ where a.org_id = ${orgId}
90
+ and a.course_id = ${courseId}
91
+ and coalesce((a.settings ->> 'published')::boolean, true)
92
+ group by a.id, a.settings, m.id, m.title, m.seq, a.seq
93
+ order by m.seq, a.seq
94
+ `)).rows.map((r) => ({
95
+ activityId: r.id,
96
+ title: r.title,
97
+ moduleId: r.module_id,
98
+ moduleTitle: r.module_title,
99
+ started: Number(r.started),
100
+ completed: Number(r.completed)
101
+ }));
102
+ return {
103
+ enrolled: Number(summary?.enrolled ?? 0),
104
+ started: Number(summary?.started ?? 0),
105
+ completed: Number(summary?.completed ?? 0),
106
+ avgProgress: Number(summary?.avg_progress ?? 0),
107
+ activities
108
+ };
109
+ }
110
+ async enrollments(orgId, courseId, days) {
111
+ if (!await this.courseExists(orgId, courseId)) return null;
112
+ return (await this.db.execute(sql`
113
+ select
114
+ to_char(d.day, 'YYYY-MM-DD') as date,
115
+ count(${entitlements.id})::int as count
116
+ from generate_series(
117
+ (now() at time zone 'utc')::date - (${days} - 1),
118
+ (now() at time zone 'utc')::date,
119
+ interval '1 day'
120
+ ) as d(day)
121
+ left join ${entitlements}
122
+ on ${entitlements.orgId} = ${orgId}
123
+ and ${entitlements.contentId} = ${courseId}
124
+ and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
125
+ group by d.day
126
+ order by d.day
127
+ `)).rows.map((r) => ({
128
+ date: r.date,
129
+ count: Number(r.count)
130
+ }));
131
+ }
132
+ };
133
+ translateDbErrors(DrizzleCourseAnalyticsRepository);
134
+ //#endregion
135
+ export { DrizzleCourseAnalyticsRepository };
136
+
137
+ //# sourceMappingURL=course-analytics.js.map