@headless-lms/core 0.1.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assets/events.d.ts +81 -0
  3. package/dist/assets/events.js +24 -0
  4. package/dist/assets/events.js.map +1 -0
  5. package/dist/assets/index.d.ts +7 -0
  6. package/dist/assets/index.js +3 -0
  7. package/dist/assets/model.d.ts +4 -0
  8. package/dist/assets/model.js +1 -0
  9. package/dist/assets/ports.d.ts +34 -0
  10. package/dist/assets/ports.js +1 -0
  11. package/dist/assets/service.d.ts +29 -0
  12. package/dist/assets/service.js +130 -0
  13. package/dist/assets/service.js.map +1 -0
  14. package/dist/automations/actions.d.ts +24 -0
  15. package/dist/automations/actions.js +59 -0
  16. package/dist/automations/actions.js.map +1 -0
  17. package/dist/automations/catalog.d.ts +12 -0
  18. package/dist/automations/catalog.js +224 -0
  19. package/dist/automations/catalog.js.map +1 -0
  20. package/dist/automations/events.d.ts +264 -0
  21. package/dist/automations/events.js +54 -0
  22. package/dist/automations/events.js.map +1 -0
  23. package/dist/automations/index.d.ts +9 -0
  24. package/dist/automations/index.js +4 -0
  25. package/dist/automations/model.d.ts +12 -0
  26. package/dist/automations/model.js +14 -0
  27. package/dist/automations/model.js.map +1 -0
  28. package/dist/automations/ports.d.ts +56 -0
  29. package/dist/automations/ports.js +1 -0
  30. package/dist/automations/service.d.ts +53 -0
  31. package/dist/automations/service.js +225 -0
  32. package/dist/automations/service.js.map +1 -0
  33. package/dist/automations/types.d.ts +3 -0
  34. package/dist/automations/types.js +1 -0
  35. package/dist/content/events.d.ts +410 -0
  36. package/dist/content/events.js +105 -0
  37. package/dist/content/events.js.map +1 -0
  38. package/dist/content/index.d.ts +8 -0
  39. package/dist/content/index.js +3 -0
  40. package/dist/content/model.d.ts +3 -0
  41. package/dist/content/model.js +1 -0
  42. package/dist/content/ports.d.ts +85 -0
  43. package/dist/content/ports.js +1 -0
  44. package/dist/content/service.d.ts +54 -0
  45. package/dist/content/service.js +479 -0
  46. package/dist/content/service.js.map +1 -0
  47. package/dist/content/types.d.ts +4 -0
  48. package/dist/content/types.js +1 -0
  49. package/dist/discussion/events.d.ts +101 -0
  50. package/dist/discussion/events.js +29 -0
  51. package/dist/discussion/events.js.map +1 -0
  52. package/dist/discussion/index.d.ts +8 -0
  53. package/dist/discussion/index.js +3 -0
  54. package/dist/discussion/model.d.ts +5 -0
  55. package/dist/discussion/model.js +1 -0
  56. package/dist/discussion/ports.d.ts +112 -0
  57. package/dist/discussion/ports.js +1 -0
  58. package/dist/discussion/service.d.ts +67 -0
  59. package/dist/discussion/service.js +343 -0
  60. package/dist/discussion/service.js.map +1 -0
  61. package/dist/discussion/types.d.ts +4 -0
  62. package/dist/discussion/types.js +1 -0
  63. package/dist/entitlements/events.d.ts +89 -0
  64. package/dist/entitlements/events.js +29 -0
  65. package/dist/entitlements/events.js.map +1 -0
  66. package/dist/entitlements/index.d.ts +7 -0
  67. package/dist/entitlements/index.js +3 -0
  68. package/dist/entitlements/model.d.ts +4 -0
  69. package/dist/entitlements/model.js +1 -0
  70. package/dist/entitlements/ports.d.ts +34 -0
  71. package/dist/entitlements/ports.js +1 -0
  72. package/dist/entitlements/service.d.ts +27 -0
  73. package/dist/entitlements/service.js +60 -0
  74. package/dist/entitlements/service.js.map +1 -0
  75. package/dist/identity/events.d.ts +63 -0
  76. package/dist/identity/events.js +24 -0
  77. package/dist/identity/events.js.map +1 -0
  78. package/dist/identity/index.d.ts +8 -0
  79. package/dist/identity/index.js +3 -0
  80. package/dist/identity/model.d.ts +3 -0
  81. package/dist/identity/model.js +1 -0
  82. package/dist/identity/ports.d.ts +58 -0
  83. package/dist/identity/ports.js +1 -0
  84. package/dist/identity/service.d.ts +38 -0
  85. package/dist/identity/service.js +79 -0
  86. package/dist/identity/service.js.map +1 -0
  87. package/dist/identity/types.d.ts +4 -0
  88. package/dist/identity/types.js +1 -0
  89. package/dist/integrations/events.d.ts +70 -0
  90. package/dist/integrations/events.js +24 -0
  91. package/dist/integrations/events.js.map +1 -0
  92. package/dist/integrations/index.d.ts +9 -0
  93. package/dist/integrations/index.js +6 -0
  94. package/dist/integrations/model.d.ts +28 -0
  95. package/dist/integrations/model.js +45 -0
  96. package/dist/integrations/model.js.map +1 -0
  97. package/dist/integrations/ports.d.ts +62 -0
  98. package/dist/integrations/ports.js +1 -0
  99. package/dist/integrations/registry.d.ts +7 -0
  100. package/dist/integrations/registry.js +16 -0
  101. package/dist/integrations/registry.js.map +1 -0
  102. package/dist/integrations/service.d.ts +47 -0
  103. package/dist/integrations/service.js +184 -0
  104. package/dist/integrations/service.js.map +1 -0
  105. package/dist/integrations/zod.d.ts +29 -0
  106. package/dist/integrations/zod.js +40 -0
  107. package/dist/integrations/zod.js.map +1 -0
  108. package/dist/organizations/events.d.ts +197 -0
  109. package/dist/organizations/events.js +59 -0
  110. package/dist/organizations/events.js.map +1 -0
  111. package/dist/organizations/index.d.ts +11 -0
  112. package/dist/organizations/index.js +5 -0
  113. package/dist/organizations/members.d.ts +32 -0
  114. package/dist/organizations/members.js +12 -0
  115. package/dist/organizations/members.js.map +1 -0
  116. package/dist/organizations/model.d.ts +3 -0
  117. package/dist/organizations/model.js +1 -0
  118. package/dist/organizations/ports.d.ts +127 -0
  119. package/dist/organizations/ports.js +1 -0
  120. package/dist/organizations/roles.d.ts +23 -0
  121. package/dist/organizations/roles.js +70 -0
  122. package/dist/organizations/roles.js.map +1 -0
  123. package/dist/organizations/service.d.ts +58 -0
  124. package/dist/organizations/service.js +359 -0
  125. package/dist/organizations/service.js.map +1 -0
  126. package/dist/organizations/types.d.ts +39 -0
  127. package/dist/organizations/types.js +1 -0
  128. package/dist/progress/events.d.ts +55 -0
  129. package/dist/progress/events.js +19 -0
  130. package/dist/progress/events.js.map +1 -0
  131. package/dist/progress/index.d.ts +8 -0
  132. package/dist/progress/index.js +3 -0
  133. package/dist/progress/model.d.ts +3 -0
  134. package/dist/progress/model.js +1 -0
  135. package/dist/progress/ports.d.ts +37 -0
  136. package/dist/progress/ports.js +1 -0
  137. package/dist/progress/service.d.ts +33 -0
  138. package/dist/progress/service.js +181 -0
  139. package/dist/progress/service.js.map +1 -0
  140. package/dist/progress/types.d.ts +4 -0
  141. package/dist/progress/types.js +1 -0
  142. package/dist/reporting/dashboard/index.d.ts +4 -0
  143. package/dist/reporting/dashboard/index.js +2 -0
  144. package/dist/reporting/dashboard/model.d.ts +14 -0
  145. package/dist/reporting/dashboard/model.js +1 -0
  146. package/dist/reporting/dashboard/ports.d.ts +13 -0
  147. package/dist/reporting/dashboard/ports.js +1 -0
  148. package/dist/reporting/dashboard/service.d.ts +19 -0
  149. package/dist/reporting/dashboard/service.js +20 -0
  150. package/dist/reporting/dashboard/service.js.map +1 -0
  151. package/dist/reporting/learn/index.d.ts +5 -0
  152. package/dist/reporting/learn/index.js +2 -0
  153. package/dist/reporting/learn/model.d.ts +23 -0
  154. package/dist/reporting/learn/model.js +1 -0
  155. package/dist/reporting/learn/ports.d.ts +42 -0
  156. package/dist/reporting/learn/ports.js +1 -0
  157. package/dist/reporting/learn/service.d.ts +48 -0
  158. package/dist/reporting/learn/service.js +111 -0
  159. package/dist/reporting/learn/service.js.map +1 -0
  160. package/dist/reporting/students/index.d.ts +4 -0
  161. package/dist/reporting/students/index.js +2 -0
  162. package/dist/reporting/students/model.d.ts +31 -0
  163. package/dist/reporting/students/model.js +1 -0
  164. package/dist/reporting/students/ports.d.ts +13 -0
  165. package/dist/reporting/students/ports.js +1 -0
  166. package/dist/reporting/students/service.d.ts +19 -0
  167. package/dist/reporting/students/service.js +20 -0
  168. package/dist/reporting/students/service.js.map +1 -0
  169. package/dist/shared/errors.d.ts +22 -0
  170. package/dist/shared/errors.js +34 -0
  171. package/dist/shared/errors.js.map +1 -0
  172. package/dist/shared/events.d.ts +31 -0
  173. package/dist/shared/events.js +75 -0
  174. package/dist/shared/events.js.map +1 -0
  175. package/dist/shared/id.d.ts +44 -0
  176. package/dist/shared/id.js +53 -0
  177. package/dist/shared/id.js.map +1 -0
  178. package/dist/shared/invite-token.d.ts +9 -0
  179. package/dist/shared/invite-token.js +16 -0
  180. package/dist/shared/invite-token.js.map +1 -0
  181. package/dist/shared/logger.d.ts +19 -0
  182. package/dist/shared/logger.js +44 -0
  183. package/dist/shared/logger.js.map +1 -0
  184. package/dist/shared/mailer.d.ts +23 -0
  185. package/dist/shared/mailer.js +27 -0
  186. package/dist/shared/mailer.js.map +1 -0
  187. package/dist/shared/ports.d.ts +62 -0
  188. package/dist/shared/ports.js +2 -0
  189. package/dist/shared/settings.d.ts +25 -0
  190. package/dist/shared/settings.js +21 -0
  191. package/dist/shared/settings.js.map +1 -0
  192. package/dist/types/assets.d.ts +2 -0
  193. package/dist/types/assets.js +1 -0
  194. package/dist/types/automations.d.ts +23 -0
  195. package/dist/types/automations.js +1 -0
  196. package/dist/types/content.d.ts +2 -0
  197. package/dist/types/content.js +1 -0
  198. package/dist/types/discussion.d.ts +36 -0
  199. package/dist/types/discussion.js +1 -0
  200. package/dist/types/email-templates.d.ts +57 -0
  201. package/dist/types/email-templates.js +1 -0
  202. package/dist/types/entitlements.d.ts +3 -0
  203. package/dist/types/entitlements.js +1 -0
  204. package/dist/types/events.d.ts +25 -0
  205. package/dist/types/events.js +1 -0
  206. package/dist/types/identity.d.ts +37 -0
  207. package/dist/types/identity.js +1 -0
  208. package/dist/types/index.d.ts +23 -0
  209. package/dist/types/index.js +1 -0
  210. package/dist/types/integrations.d.ts +45 -0
  211. package/dist/types/integrations.js +1 -0
  212. package/dist/types/organizations.d.ts +15 -0
  213. package/dist/types/organizations.js +1 -0
  214. package/dist/types/ports.d.ts +57 -0
  215. package/dist/types/ports.js +1 -0
  216. package/dist/types/progress.d.ts +6 -0
  217. package/dist/types/progress.js +1 -0
  218. package/dist/types/schemas/assets.d.ts +109 -0
  219. package/dist/types/schemas/assets.js +49 -0
  220. package/dist/types/schemas/assets.js.map +1 -0
  221. package/dist/types/schemas/automations.d.ts +118 -0
  222. package/dist/types/schemas/automations.js +73 -0
  223. package/dist/types/schemas/automations.js.map +1 -0
  224. package/dist/types/schemas/content.d.ts +216 -0
  225. package/dist/types/schemas/content.js +138 -0
  226. package/dist/types/schemas/content.js.map +1 -0
  227. package/dist/types/schemas/credentials.d.ts +15 -0
  228. package/dist/types/schemas/credentials.js +15 -0
  229. package/dist/types/schemas/credentials.js.map +1 -0
  230. package/dist/types/schemas/discussion.d.ts +192 -0
  231. package/dist/types/schemas/discussion.js +100 -0
  232. package/dist/types/schemas/discussion.js.map +1 -0
  233. package/dist/types/schemas/entitlements.d.ts +50 -0
  234. package/dist/types/schemas/entitlements.js +38 -0
  235. package/dist/types/schemas/entitlements.js.map +1 -0
  236. package/dist/types/schemas/identity.d.ts +37 -0
  237. package/dist/types/schemas/identity.js +34 -0
  238. package/dist/types/schemas/identity.js.map +1 -0
  239. package/dist/types/schemas/index.d.ts +14 -0
  240. package/dist/types/schemas/index.js +14 -0
  241. package/dist/types/schemas/integrations.d.ts +36 -0
  242. package/dist/types/schemas/integrations.js +30 -0
  243. package/dist/types/schemas/integrations.js.map +1 -0
  244. package/dist/types/schemas/organizations.d.ts +135 -0
  245. package/dist/types/schemas/organizations.js +90 -0
  246. package/dist/types/schemas/organizations.js.map +1 -0
  247. package/dist/types/schemas/outbox.d.ts +19 -0
  248. package/dist/types/schemas/outbox.js +18 -0
  249. package/dist/types/schemas/outbox.js.map +1 -0
  250. package/dist/types/schemas/progress.d.ts +53 -0
  251. package/dist/types/schemas/progress.js +37 -0
  252. package/dist/types/schemas/progress.js.map +1 -0
  253. package/dist/types/schemas/settings.d.ts +16 -0
  254. package/dist/types/schemas/settings.js +15 -0
  255. package/dist/types/schemas/settings.js.map +1 -0
  256. package/dist/types/schemas/shared.d.ts +13 -0
  257. package/dist/types/schemas/shared.js +28 -0
  258. package/dist/types/schemas/shared.js.map +1 -0
  259. package/dist/types/shared.d.ts +25 -0
  260. package/dist/types/shared.js +1 -0
  261. package/package.json +61 -0
@@ -0,0 +1,54 @@
1
+ import { Page } from "../types/shared.js";
2
+ import { Logger } from "../types/ports.js";
3
+ import { Activity, AddDownloadAssetInput, Course, CourseSettings, CreateCourseInput, CreateDownloadInput, Download, DownloadAsset, ListCoursesQuery, ListDownloadsQuery, Module, SaveActivityInput, UpdateCourseInput, UpdateDownloadInput } from "../types/schemas/content.js";
4
+ import "../shared/ports.js";
5
+ import "./model.js";
6
+ import "./types.js";
7
+ import { ContentRepository, ContentService, ContentUnitOfWork } from "./ports.js";
8
+ import { SettingsService } from "../shared/settings.js";
9
+ //#region src/content/service.d.ts
10
+ type ContentServiceParams = {
11
+ repo: ContentRepository;
12
+ uow: ContentUnitOfWork;
13
+ settings: SettingsService;
14
+ logger?: Logger;
15
+ };
16
+ declare class ContentServiceImpl implements ContentService {
17
+ private readonly repo;
18
+ private readonly uow;
19
+ private readonly settings;
20
+ private readonly logger;
21
+ constructor(params: ContentServiceParams);
22
+ list(orgId: string, query: ListCoursesQuery): Promise<Page<Course>>;
23
+ getCourse(orgId: string, id: string): Promise<Course | null>;
24
+ createCourse(orgId: string, input: CreateCourseInput): Promise<Course>;
25
+ updateCourse(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course>;
26
+ patchSettings(orgId: string, id: string, value: Partial<CourseSettings>): Promise<CourseSettings>;
27
+ deleteCourse(orgId: string, id: string): Promise<void>;
28
+ listCourseModules(orgId: string, courseId: string): Promise<Module[]>;
29
+ listCourseActivities(orgId: string, courseId: string): Promise<Activity[]>;
30
+ getActivity(orgId: string, activityId: string): Promise<Activity | null>;
31
+ getModule(orgId: string, moduleId: string): Promise<Module | null>;
32
+ reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]>;
33
+ createModule(orgId: string, courseId: string, title: string): Promise<Module[]>;
34
+ updateModule(orgId: string, courseId: string, moduleId: string, title: string): Promise<Module[]>;
35
+ deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]>;
36
+ reorderActivities(orgId: string, courseId: string, moduleId: string, orderedIds: string[]): Promise<Module[]>;
37
+ saveActivity(orgId: string, courseId: string, moduleId: string, input: SaveActivityInput, activityId?: string): Promise<Module[]>;
38
+ deleteActivity(orgId: string, courseId: string, moduleId: string, activityId: string): Promise<Module[]>;
39
+ listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>>;
40
+ getDownload(orgId: string, id: string): Promise<Download | null>;
41
+ createDownload(orgId: string, input: CreateDownloadInput): Promise<Download>;
42
+ updateDownload(orgId: string, id: string, patch: UpdateDownloadInput): Promise<Download>;
43
+ removeDownload(orgId: string, id: string): Promise<void>;
44
+ listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]>;
45
+ addDownloadAsset(orgId: string, downloadId: string, input: AddDownloadAssetInput): Promise<DownloadAsset[]>;
46
+ removeDownloadAsset(orgId: string, downloadId: string, assetId: string): Promise<DownloadAsset[]>;
47
+ renameDownloadAsset(orgId: string, downloadId: string, assetId: string, displayName: string | null): Promise<DownloadAsset[]>;
48
+ /** The caller must send the COMPLETE ordered set — a partial list would
49
+ * silently unlink whatever it omitted. */
50
+ reorderDownloadAssets(orgId: string, downloadId: string, assetIds: string[]): Promise<DownloadAsset[]>;
51
+ }
52
+ //#endregion
53
+ export { ContentServiceImpl, ContentServiceParams };
54
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1,479 @@
1
+ import { noopLogger } from "../shared/logger.js";
2
+ import { contentEvents } from "./events.js";
3
+ import "../shared/settings.js";
4
+ import { ConflictError, NotFoundError } from "../shared/errors.js";
5
+ //#region src/content/service.ts
6
+ /** URL-safe slug derived from a title. Domain rule owned by the service. */
7
+ function slugify(title) {
8
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
9
+ }
10
+ /** Defaults for keys the stored blob doesn't carry (rows predating a setting). */
11
+ const settingsDefaults = { transcriptDownloads: false };
12
+ function toCourseSettings(stored) {
13
+ return {
14
+ ...settingsDefaults,
15
+ ...stored
16
+ };
17
+ }
18
+ var ContentServiceImpl = class {
19
+ repo;
20
+ uow;
21
+ settings;
22
+ logger;
23
+ constructor(params) {
24
+ this.repo = params.repo;
25
+ this.uow = params.uow;
26
+ this.settings = params.settings;
27
+ this.logger = params.logger ?? noopLogger;
28
+ }
29
+ list(orgId, query) {
30
+ return this.repo.list(orgId, query);
31
+ }
32
+ async getCourse(orgId, id) {
33
+ const course = await this.repo.findById(orgId, id);
34
+ if (!course) return null;
35
+ const settings = await this.settings.get(orgId, "content", id);
36
+ return {
37
+ ...course,
38
+ settings: {
39
+ ...toCourseSettings(course.settings),
40
+ ...settings
41
+ }
42
+ };
43
+ }
44
+ async createCourse(orgId, input) {
45
+ const course = await this.uow.run(async ({ content, outbox }) => {
46
+ const created = await content.create(orgId, input, slugify(input.title));
47
+ await outbox.append([contentEvents.courseCreated.make({
48
+ orgId,
49
+ data: created
50
+ })]);
51
+ return created;
52
+ });
53
+ this.logger.info("course created", {
54
+ orgId,
55
+ courseId: course.id,
56
+ title: course.title
57
+ });
58
+ return course;
59
+ }
60
+ async updateCourse(orgId, id, patch) {
61
+ const course = await this.uow.run(async ({ content, outbox }) => {
62
+ const updated = await content.update(orgId, id, patch);
63
+ if (!updated) {
64
+ this.logger.warn("course update rejected — not found", {
65
+ orgId,
66
+ courseId: id
67
+ });
68
+ throw new NotFoundError("Course", id);
69
+ }
70
+ await outbox.append([contentEvents.courseUpdated.make({
71
+ orgId,
72
+ data: updated
73
+ })]);
74
+ return updated;
75
+ });
76
+ this.logger.info("course updated", {
77
+ orgId,
78
+ courseId: id,
79
+ fields: Object.keys(patch)
80
+ });
81
+ return course;
82
+ }
83
+ async patchSettings(orgId, id, value) {
84
+ const course = await this.repo.findById(orgId, id);
85
+ if (!course) {
86
+ this.logger.warn("course settings patch rejected — not found", {
87
+ orgId,
88
+ courseId: id
89
+ });
90
+ throw new NotFoundError("Course", id);
91
+ }
92
+ const merged = await this.settings.patch(orgId, "content", id, value);
93
+ this.logger.info("course settings patched", {
94
+ orgId,
95
+ courseId: id,
96
+ value
97
+ });
98
+ return {
99
+ ...toCourseSettings(course.settings),
100
+ ...merged
101
+ };
102
+ }
103
+ async deleteCourse(orgId, id) {
104
+ await this.uow.run(async ({ content, outbox }) => {
105
+ const course = await content.findById(orgId, id);
106
+ if (!course) {
107
+ this.logger.warn("course delete rejected — not found", {
108
+ orgId,
109
+ courseId: id
110
+ });
111
+ throw new NotFoundError("Course", id);
112
+ }
113
+ if (!await content.delete(orgId, id)) throw new NotFoundError("Course", id);
114
+ await outbox.append([contentEvents.courseDeleted.make({
115
+ orgId,
116
+ data: course
117
+ })]);
118
+ });
119
+ this.logger.info("course deleted", {
120
+ orgId,
121
+ courseId: id
122
+ });
123
+ }
124
+ listCourseModules(orgId, courseId) {
125
+ return this.repo.listForCourse(orgId, courseId);
126
+ }
127
+ listCourseActivities(orgId, courseId) {
128
+ return this.repo.listActivitiesForCourse(orgId, courseId);
129
+ }
130
+ getActivity(orgId, activityId) {
131
+ return this.repo.findActivity(orgId, activityId);
132
+ }
133
+ getModule(orgId, moduleId) {
134
+ return this.repo.findModule(orgId, moduleId);
135
+ }
136
+ async reorderModules(orgId, courseId, orderedIds) {
137
+ const modules = await this.uow.run(async ({ content, outbox }) => {
138
+ const reordered = await content.reorderModules(orgId, courseId, orderedIds);
139
+ await outbox.append([contentEvents.modulesReordered.make({
140
+ orgId,
141
+ data: reordered
142
+ })]);
143
+ return reordered;
144
+ });
145
+ this.logger.debug("modules reordered", {
146
+ orgId,
147
+ courseId,
148
+ moduleIds: modules.map((m) => m.id)
149
+ });
150
+ return modules;
151
+ }
152
+ async createModule(orgId, courseId, title) {
153
+ const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
154
+ const all = await content.createModule(orgId, courseId, title);
155
+ const module = all.at(-1);
156
+ if (!module) throw new Error("failed to load created module");
157
+ await outbox.append([contentEvents.moduleCreated.make({
158
+ orgId,
159
+ data: module
160
+ })]);
161
+ return {
162
+ modules: all,
163
+ created: module
164
+ };
165
+ });
166
+ this.logger.info("module created", {
167
+ orgId,
168
+ courseId,
169
+ moduleId: created.id,
170
+ title
171
+ });
172
+ return modules;
173
+ }
174
+ async updateModule(orgId, courseId, moduleId, title) {
175
+ const modules = await this.uow.run(async ({ content, outbox }) => {
176
+ const existing = await content.findModule(orgId, moduleId);
177
+ if (!existing || existing.courseId !== courseId) {
178
+ this.logger.warn("module update rejected — not found in course", {
179
+ orgId,
180
+ courseId,
181
+ moduleId
182
+ });
183
+ throw new NotFoundError("Module", moduleId);
184
+ }
185
+ const all = await content.updateModule(orgId, courseId, moduleId, title);
186
+ const module = all.find((m) => m.id === moduleId);
187
+ if (!module) throw new NotFoundError("Module", moduleId);
188
+ await outbox.append([contentEvents.moduleUpdated.make({
189
+ orgId,
190
+ data: module
191
+ })]);
192
+ return all;
193
+ });
194
+ this.logger.info("module updated", {
195
+ orgId,
196
+ courseId,
197
+ moduleId,
198
+ title
199
+ });
200
+ return modules;
201
+ }
202
+ async deleteModule(orgId, courseId, moduleId) {
203
+ const modules = await this.uow.run(async ({ content, outbox }) => {
204
+ const module = await content.findModule(orgId, moduleId);
205
+ if (!module || module.courseId !== courseId) {
206
+ this.logger.warn("module delete rejected — not found in course", {
207
+ orgId,
208
+ courseId,
209
+ moduleId
210
+ });
211
+ throw new NotFoundError("Module", moduleId);
212
+ }
213
+ const remaining = await content.deleteModule(orgId, courseId, moduleId);
214
+ await outbox.append([contentEvents.moduleDeleted.make({
215
+ orgId,
216
+ data: module
217
+ })]);
218
+ return remaining;
219
+ });
220
+ this.logger.info("module deleted", {
221
+ orgId,
222
+ courseId,
223
+ moduleId
224
+ });
225
+ return modules;
226
+ }
227
+ async reorderActivities(orgId, courseId, moduleId, orderedIds) {
228
+ const modules = await this.uow.run(async ({ content, outbox }) => {
229
+ const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
230
+ const module = reordered.find((m) => m.id === moduleId);
231
+ if (!module) throw new NotFoundError("Module", moduleId);
232
+ await outbox.append([contentEvents.activitiesReordered.make({
233
+ orgId,
234
+ data: module
235
+ })]);
236
+ return reordered;
237
+ });
238
+ this.logger.debug("activities reordered", {
239
+ orgId,
240
+ courseId,
241
+ moduleId
242
+ });
243
+ return modules;
244
+ }
245
+ async saveActivity(orgId, courseId, moduleId, input, activityId) {
246
+ const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
247
+ if (activityId) {
248
+ const existing = await content.findActivity(orgId, activityId);
249
+ if (!existing || existing.moduleId !== moduleId || existing.courseId !== courseId) {
250
+ this.logger.warn("activity update rejected — not found in module", {
251
+ orgId,
252
+ courseId,
253
+ moduleId,
254
+ activityId
255
+ });
256
+ throw new NotFoundError("Activity", activityId);
257
+ }
258
+ } else {
259
+ const module = await content.findModule(orgId, moduleId);
260
+ if (!module || module.courseId !== courseId) {
261
+ this.logger.warn("activity create rejected — module not found in course", {
262
+ orgId,
263
+ courseId,
264
+ moduleId
265
+ });
266
+ throw new NotFoundError("Module", moduleId);
267
+ }
268
+ }
269
+ const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
270
+ await outbox.append([activityId ? contentEvents.activityUpdated.make({
271
+ orgId,
272
+ data: saved
273
+ }) : contentEvents.activityCreated.make({
274
+ orgId,
275
+ data: saved
276
+ })]);
277
+ return {
278
+ modules: all,
279
+ activity: saved
280
+ };
281
+ });
282
+ this.logger.info(activityId ? "activity updated" : "activity created", {
283
+ orgId,
284
+ courseId,
285
+ moduleId,
286
+ activityId: activity.id
287
+ });
288
+ return modules;
289
+ }
290
+ async deleteActivity(orgId, courseId, moduleId, activityId) {
291
+ const modules = await this.uow.run(async ({ content, outbox }) => {
292
+ const activity = await content.findActivity(orgId, activityId);
293
+ if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
294
+ this.logger.warn("activity delete rejected — not found in module", {
295
+ orgId,
296
+ courseId,
297
+ moduleId,
298
+ activityId
299
+ });
300
+ throw new NotFoundError("Activity", activityId);
301
+ }
302
+ const remaining = await content.deleteActivity(orgId, courseId, moduleId, activityId);
303
+ await outbox.append([contentEvents.activityDeleted.make({
304
+ orgId,
305
+ data: activity
306
+ })]);
307
+ return remaining;
308
+ });
309
+ this.logger.info("activity deleted", {
310
+ orgId,
311
+ courseId,
312
+ moduleId,
313
+ activityId
314
+ });
315
+ return modules;
316
+ }
317
+ listDownloads(orgId, query) {
318
+ return this.repo.listDownloads(orgId, query);
319
+ }
320
+ getDownload(orgId, id) {
321
+ return this.repo.getDownload(orgId, id);
322
+ }
323
+ async createDownload(orgId, input) {
324
+ const download = await this.uow.run(async ({ content, outbox }) => {
325
+ const created = await content.createDownload(orgId, input, slugify(input.title));
326
+ await outbox.append([contentEvents.downloadCreated.make({
327
+ orgId,
328
+ data: created
329
+ })]);
330
+ return created;
331
+ });
332
+ this.logger.info("download created", {
333
+ orgId,
334
+ downloadId: download.id,
335
+ title: download.title
336
+ });
337
+ return download;
338
+ }
339
+ async updateDownload(orgId, id, patch) {
340
+ const download = await this.uow.run(async ({ content, outbox }) => {
341
+ const updated = await content.updateDownload(orgId, id, patch);
342
+ if (!updated) {
343
+ this.logger.warn("download update rejected — not found", {
344
+ orgId,
345
+ downloadId: id
346
+ });
347
+ throw new NotFoundError("Download", id);
348
+ }
349
+ await outbox.append([contentEvents.downloadUpdated.make({
350
+ orgId,
351
+ data: updated
352
+ })]);
353
+ return updated;
354
+ });
355
+ this.logger.info("download updated", {
356
+ orgId,
357
+ downloadId: id,
358
+ fields: Object.keys(patch)
359
+ });
360
+ return download;
361
+ }
362
+ async removeDownload(orgId, id) {
363
+ await this.uow.run(async ({ content, outbox }) => {
364
+ const download = await content.getDownload(orgId, id);
365
+ if (!download) {
366
+ this.logger.warn("download delete rejected — not found", {
367
+ orgId,
368
+ downloadId: id
369
+ });
370
+ throw new NotFoundError("Download", id);
371
+ }
372
+ if (!await content.deleteDownload(orgId, id)) throw new NotFoundError("Download", id);
373
+ await outbox.append([contentEvents.downloadDeleted.make({
374
+ orgId,
375
+ data: download
376
+ })]);
377
+ });
378
+ this.logger.info("download deleted", {
379
+ orgId,
380
+ downloadId: id
381
+ });
382
+ }
383
+ listDownloadAssets(orgId, downloadId) {
384
+ return this.repo.listDownloadAssets(orgId, downloadId);
385
+ }
386
+ async addDownloadAsset(orgId, downloadId, input) {
387
+ const assets = await this.uow.run(async ({ content, outbox }) => {
388
+ const assets = await content.addDownloadAsset(orgId, downloadId, input);
389
+ await outbox.append([contentEvents.downloadAssetAdded.make({
390
+ orgId,
391
+ data: {
392
+ downloadId,
393
+ assets
394
+ }
395
+ })]);
396
+ return assets;
397
+ });
398
+ this.logger.info("download asset added", {
399
+ orgId,
400
+ downloadId,
401
+ assetId: input.assetId
402
+ });
403
+ return assets;
404
+ }
405
+ async removeDownloadAsset(orgId, downloadId, assetId) {
406
+ const assets = await this.uow.run(async ({ content, outbox }) => {
407
+ const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
408
+ await outbox.append([contentEvents.downloadAssetRemoved.make({
409
+ orgId,
410
+ data: {
411
+ downloadId,
412
+ assets
413
+ }
414
+ })]);
415
+ return assets;
416
+ });
417
+ this.logger.info("download asset removed", {
418
+ orgId,
419
+ downloadId,
420
+ assetId
421
+ });
422
+ return assets;
423
+ }
424
+ async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
425
+ const assets = await this.uow.run(async ({ content, outbox }) => {
426
+ const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
427
+ await outbox.append([contentEvents.downloadAssetRenamed.make({
428
+ orgId,
429
+ data: {
430
+ downloadId,
431
+ assets
432
+ }
433
+ })]);
434
+ return assets;
435
+ });
436
+ this.logger.info("download asset renamed", {
437
+ orgId,
438
+ downloadId,
439
+ assetId
440
+ });
441
+ return assets;
442
+ }
443
+ /** The caller must send the COMPLETE ordered set — a partial list would
444
+ * silently unlink whatever it omitted. */
445
+ async reorderDownloadAssets(orgId, downloadId, assetIds) {
446
+ const assets = await this.uow.run(async ({ content, outbox }) => {
447
+ const current = await content.listDownloadAssets(orgId, downloadId);
448
+ const currentIds = new Set(current.map((a) => a.assetId));
449
+ const requestedIds = new Set(assetIds);
450
+ if (requestedIds.size !== assetIds.length || requestedIds.size !== currentIds.size || !assetIds.every((id) => currentIds.has(id))) {
451
+ this.logger.warn("download asset reorder rejected — id set mismatch", {
452
+ orgId,
453
+ downloadId,
454
+ assetIds,
455
+ currentIds: [...currentIds]
456
+ });
457
+ throw new ConflictError("Ordered asset ids does not match the download's current assets");
458
+ }
459
+ const assets = await content.reorderDownloadAssets(orgId, downloadId, assetIds);
460
+ await outbox.append([contentEvents.downloadAssetsReordered.make({
461
+ orgId,
462
+ data: {
463
+ downloadId,
464
+ assets
465
+ }
466
+ })]);
467
+ return assets;
468
+ });
469
+ this.logger.debug("download assets reordered", {
470
+ orgId,
471
+ downloadId
472
+ });
473
+ return assets;
474
+ }
475
+ };
476
+ //#endregion
477
+ export { ContentServiceImpl };
478
+
479
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/content/service.ts"],"sourcesContent":["import type {\n Activity,\n Course,\n CourseSettings,\n Download,\n DownloadAsset,\n Module,\n SaveActivityInput,\n} from './model.js';\nimport type { ContentService, ContentRepository, ContentUnitOfWork } from './ports.js';\nimport type {\n AddDownloadAssetInput,\n CreateCourseInput,\n CreateDownloadInput,\n ListCoursesQuery,\n ListDownloadsQuery,\n Page,\n UpdateCourseInput,\n UpdateDownloadInput,\n} from './types.js';\nimport { contentEvents } from './events.js';\nimport type { Logger } from '../shared/ports.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport { NotFoundError, ConflictError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\n\n/** URL-safe slug derived from a title. Domain rule owned by the service. */\nfunction slugify(title: string): string {\n return title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '');\n}\n\n/** Defaults for keys the stored blob doesn't carry (rows predating a setting). */\nconst settingsDefaults: CourseSettings = { transcriptDownloads: false };\n\nfunction toCourseSettings(stored: unknown): CourseSettings {\n return { ...settingsDefaults, ...(stored as Partial<CourseSettings> | null) };\n}\n\nexport type ContentServiceParams = {\n repo: ContentRepository;\n uow: ContentUnitOfWork;\n settings: SettingsService;\n logger?: Logger;\n};\n\nexport class ContentServiceImpl implements ContentService {\n private readonly repo: ContentRepository;\n private readonly uow: ContentUnitOfWork;\n private readonly settings: SettingsService;\n private readonly logger: Logger;\n\n constructor(params: ContentServiceParams) {\n this.repo = params.repo;\n this.uow = params.uow;\n this.settings = params.settings;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: ListCoursesQuery): Promise<Page<Course>> {\n return this.repo.list(orgId, query);\n }\n\n async getCourse(orgId: string, id: string): Promise<Course | null> {\n const course = await this.repo.findById(orgId, id);\n if (!course) {\n return null;\n }\n const settings = await this.settings.get<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n id,\n );\n\n return {\n ...course,\n settings: {\n ...toCourseSettings(course.settings),\n ...settings,\n } as Course['settings'],\n };\n }\n\n async createCourse(orgId: string, input: CreateCourseInput): Promise<Course> {\n const course = await this.uow.run(async ({ content, outbox }) => {\n const created = await content.create(orgId, input, slugify(input.title));\n await outbox.append([\n contentEvents.courseCreated.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('course created', { orgId, courseId: course.id, title: course.title });\n return course;\n }\n\n async updateCourse(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course> {\n const course = await this.uow.run(async ({ content, outbox }) => {\n const updated = await content.update(orgId, id, patch);\n if (!updated) {\n this.logger.warn('course update rejected — not found', { orgId, courseId: id });\n throw new NotFoundError('Course', id);\n }\n await outbox.append([\n contentEvents.courseUpdated.make({ orgId, data: updated }),\n ]);\n return updated;\n });\n this.logger.info('course updated', { orgId, courseId: id, fields: Object.keys(patch) });\n return course;\n }\n\n async patchSettings(\n orgId: string,\n id: string,\n value: Partial<CourseSettings>,\n ): Promise<CourseSettings> {\n const course = await this.repo.findById(orgId, id);\n if (!course) {\n this.logger.warn('course settings patch rejected — not found', { orgId, courseId: id });\n throw new NotFoundError('Course', id);\n }\n const merged = await this.settings.patch<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n id,\n value,\n );\n this.logger.info('course settings patched', { orgId, courseId: id, value });\n return { ...toCourseSettings(course.settings), ...merged };\n }\n\n async deleteCourse(orgId: string, id: string): Promise<void> {\n await this.uow.run(async ({ content, outbox }) => {\n // Snapshot before the delete — the event carries the last known state.\n const course = await content.findById(orgId, id);\n if (!course) {\n this.logger.warn('course delete rejected — not found', { orgId, courseId: id });\n throw new NotFoundError('Course', id);\n }\n const ok = await content.delete(orgId, id);\n if (!ok) {\n throw new NotFoundError('Course', id);\n }\n await outbox.append([contentEvents.courseDeleted.make({ orgId, data: course })]);\n });\n this.logger.info('course deleted', { orgId, courseId: id });\n }\n\n // --- modules & activities ------------------------------------------------\n\n listCourseModules(orgId: string, courseId: string): Promise<Module[]> {\n return this.repo.listForCourse(orgId, courseId);\n }\n listCourseActivities(orgId: string, courseId: string): Promise<Activity[]> {\n return this.repo.listActivitiesForCourse(orgId, courseId);\n }\n getActivity(orgId: string, activityId: string): Promise<Activity | null> {\n return this.repo.findActivity(orgId, activityId);\n }\n getModule(orgId: string, moduleId: string): Promise<Module | null> {\n return this.repo.findModule(orgId, moduleId);\n }\n\n async reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]> {\n const modules = await this.uow.run(async ({ content, outbox }) => {\n const reordered = await content.reorderModules(orgId, courseId, orderedIds);\n await outbox.append([\n contentEvents.modulesReordered.make({ orgId, data: reordered }),\n ]);\n return reordered;\n });\n this.logger.debug('modules reordered', {\n orgId,\n courseId,\n moduleIds: modules.map((m) => m.id),\n });\n return modules;\n }\n\n async createModule(orgId: string, courseId: string, title: string): Promise<Module[]> {\n const { modules, created } = await this.uow.run(async ({ content, outbox }) => {\n const all = await content.createModule(orgId, courseId, title);\n // The list is ordered by seq and the new module is appended at max(seq)+1.\n const module = all.at(-1);\n if (!module) {\n throw new Error('failed to load created module');\n }\n await outbox.append([contentEvents.moduleCreated.make({ orgId, data: module })]);\n return { modules: all, created: module };\n });\n this.logger.info('module created', { orgId, courseId, moduleId: created.id, title });\n return modules;\n }\n\n async updateModule(\n orgId: string,\n courseId: string,\n moduleId: string,\n title: string,\n ): Promise<Module[]> {\n const modules = await this.uow.run(async ({ content, outbox }) => {\n const existing = await content.findModule(orgId, moduleId);\n if (!existing || existing.courseId !== courseId) {\n this.logger.warn('module update rejected — not found in course', {\n orgId,\n courseId,\n moduleId,\n });\n throw new NotFoundError('Module', moduleId);\n }\n const all = await content.updateModule(orgId, courseId, moduleId, title);\n const module = all.find((m) => m.id === moduleId);\n if (!module) {\n throw new NotFoundError('Module', moduleId);\n }\n await outbox.append([contentEvents.moduleUpdated.make({ orgId, data: module })]);\n return all;\n });\n this.logger.info('module updated', { orgId, courseId, moduleId, title });\n return modules;\n }\n\n async deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]> {\n const modules = await this.uow.run(async ({ content, outbox }) => {\n // Snapshot before the delete — the event carries the last known state.\n const module = await content.findModule(orgId, moduleId);\n if (!module || module.courseId !== courseId) {\n this.logger.warn('module delete rejected — not found in course', {\n orgId,\n courseId,\n moduleId,\n });\n throw new NotFoundError('Module', moduleId);\n }\n const remaining = await content.deleteModule(orgId, courseId, moduleId);\n await outbox.append([contentEvents.moduleDeleted.make({ orgId, data: module })]);\n return remaining;\n });\n this.logger.info('module deleted', { orgId, courseId, moduleId });\n return modules;\n }\n\n async reorderActivities(\n orgId: string,\n courseId: string,\n moduleId: string,\n orderedIds: string[],\n ): Promise<Module[]> {\n const modules = await this.uow.run(async ({ content, outbox }) => {\n const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);\n const module = reordered.find((m) => m.id === moduleId);\n if (!module) {\n throw new NotFoundError('Module', moduleId);\n }\n await outbox.append([\n contentEvents.activitiesReordered.make({ orgId, data: module }),\n ]);\n return reordered;\n });\n this.logger.debug('activities reordered', { orgId, courseId, moduleId });\n return modules;\n }\n\n async saveActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n input: SaveActivityInput,\n activityId?: string,\n ): Promise<Module[]> {\n const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {\n if (activityId) {\n const existing = await content.findActivity(orgId, activityId);\n if (!existing || existing.moduleId !== moduleId || existing.courseId !== courseId) {\n this.logger.warn('activity update rejected — not found in module', {\n orgId,\n courseId,\n moduleId,\n activityId,\n });\n throw new NotFoundError('Activity', activityId);\n }\n } else {\n const module = await content.findModule(orgId, moduleId);\n if (!module || module.courseId !== courseId) {\n this.logger.warn('activity create rejected — module not found in course', {\n orgId,\n courseId,\n moduleId,\n });\n throw new NotFoundError('Module', moduleId);\n }\n }\n\n const { modules: all, activity: saved } = await content.saveActivity(\n orgId,\n courseId,\n moduleId,\n input,\n activityId,\n );\n await outbox.append([\n activityId\n ? contentEvents.activityUpdated.make({ orgId, data: saved })\n : contentEvents.activityCreated.make({ orgId, data: saved }),\n ]);\n return { modules: all, activity: saved };\n });\n this.logger.info(activityId ? 'activity updated' : 'activity created', {\n orgId,\n courseId,\n moduleId,\n activityId: activity.id,\n });\n return modules;\n }\n\n async deleteActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n activityId: string,\n ): Promise<Module[]> {\n const modules = await this.uow.run(async ({ content, outbox }) => {\n // Snapshot before the delete — the event carries the last known state.\n const activity = await content.findActivity(orgId, activityId);\n if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {\n this.logger.warn('activity delete rejected — not found in module', {\n orgId,\n courseId,\n moduleId,\n activityId,\n });\n throw new NotFoundError('Activity', activityId);\n }\n const remaining = await content.deleteActivity(orgId, courseId, moduleId, activityId);\n await outbox.append([\n contentEvents.activityDeleted.make({ orgId, data: activity }),\n ]);\n return remaining;\n });\n this.logger.info('activity deleted', { orgId, courseId, moduleId, activityId });\n return modules;\n }\n\n // --- downloads ------------------------------------------------------------\n\n listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>> {\n return this.repo.listDownloads(orgId, query);\n }\n\n getDownload(orgId: string, id: string): Promise<Download | null> {\n return this.repo.getDownload(orgId, id);\n }\n\n async createDownload(orgId: string, input: CreateDownloadInput): Promise<Download> {\n const download = await this.uow.run(async ({ content, outbox }) => {\n const created = await content.createDownload(orgId, input, slugify(input.title));\n await outbox.append([\n contentEvents.downloadCreated.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('download created', { orgId, downloadId: download.id, title: download.title });\n return download;\n }\n\n async updateDownload(orgId: string, id: string, patch: UpdateDownloadInput): Promise<Download> {\n const download = await this.uow.run(async ({ content, outbox }) => {\n const updated = await content.updateDownload(orgId, id, patch);\n if (!updated) {\n this.logger.warn('download update rejected — not found', { orgId, downloadId: id });\n throw new NotFoundError('Download', id);\n }\n await outbox.append([\n contentEvents.downloadUpdated.make({ orgId, data: updated }),\n ]);\n return updated;\n });\n this.logger.info('download updated', { orgId, downloadId: id, fields: Object.keys(patch) });\n return download;\n }\n\n async removeDownload(orgId: string, id: string): Promise<void> {\n await this.uow.run(async ({ content, outbox }) => {\n // Snapshot before the delete — the event carries the last known state.\n const download = await content.getDownload(orgId, id);\n if (!download) {\n this.logger.warn('download delete rejected — not found', { orgId, downloadId: id });\n throw new NotFoundError('Download', id);\n }\n const ok = await content.deleteDownload(orgId, id);\n if (!ok) {\n throw new NotFoundError('Download', id);\n }\n await outbox.append([\n contentEvents.downloadDeleted.make({ orgId, data: download }),\n ]);\n });\n this.logger.info('download deleted', { orgId, downloadId: id });\n }\n\n // --- download asset links ------------------------------------------------\n\n listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]> {\n return this.repo.listDownloadAssets(orgId, downloadId);\n }\n\n async addDownloadAsset(\n orgId: string,\n downloadId: string,\n input: AddDownloadAssetInput,\n ): Promise<DownloadAsset[]> {\n const assets = await this.uow.run(async ({ content, outbox }) => {\n const assets = await content.addDownloadAsset(orgId, downloadId, input);\n await outbox.append([\n contentEvents.downloadAssetAdded.make({ orgId, data: { downloadId, assets } }),\n ]);\n return assets;\n });\n this.logger.info('download asset added', { orgId, downloadId, assetId: input.assetId });\n return assets;\n }\n\n async removeDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n ): Promise<DownloadAsset[]> {\n const assets = await this.uow.run(async ({ content, outbox }) => {\n const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);\n await outbox.append([\n contentEvents.downloadAssetRemoved.make({ orgId, data: { downloadId, assets } }),\n ]);\n return assets;\n });\n this.logger.info('download asset removed', { orgId, downloadId, assetId });\n return assets;\n }\n\n async renameDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n displayName: string | null,\n ): Promise<DownloadAsset[]> {\n const assets = await this.uow.run(async ({ content, outbox }) => {\n const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);\n await outbox.append([\n contentEvents.downloadAssetRenamed.make({ orgId, data: { downloadId, assets } }),\n ]);\n return assets;\n });\n this.logger.info('download asset renamed', { orgId, downloadId, assetId });\n return assets;\n }\n\n /** The caller must send the COMPLETE ordered set — a partial list would\n * silently unlink whatever it omitted. */\n async reorderDownloadAssets(\n orgId: string,\n downloadId: string,\n assetIds: string[],\n ): Promise<DownloadAsset[]> {\n const assets = await this.uow.run(async ({ content, outbox }) => {\n const current = await content.listDownloadAssets(orgId, downloadId);\n const currentIds = new Set(current.map((a) => a.assetId));\n const requestedIds = new Set(assetIds);\n if (\n requestedIds.size !== assetIds.length ||\n requestedIds.size !== currentIds.size ||\n !assetIds.every((id) => currentIds.has(id))\n ) {\n this.logger.warn('download asset reorder rejected — id set mismatch', {\n orgId,\n downloadId,\n assetIds,\n currentIds: [...currentIds],\n });\n throw new ConflictError(\"Ordered asset ids does not match the download's current assets\");\n }\n const assets = await content.reorderDownloadAssets(orgId, downloadId, assetIds);\n await outbox.append([\n contentEvents.downloadAssetsReordered.make({ orgId, data: { downloadId, assets } }),\n ]);\n return assets;\n });\n this.logger.debug('download assets reordered', { orgId, downloadId });\n return assets;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { Page } from "../types/shared.js";
2
+ import { AddDownloadAssetInput, CreateCourseInput, CreateDownloadInput, ListCoursesQuery, ListDownloadsQuery, ReorderDownloadAssetsInput, UpdateCourseInput, UpdateDownloadInput } from "../types/schemas/content.js";
3
+ import "../types/index.js";
4
+ export type { AddDownloadAssetInput, CreateCourseInput, CreateDownloadInput, ListCoursesQuery, ListDownloadsQuery, Page, ReorderDownloadAssetsInput, UpdateCourseInput, UpdateDownloadInput };
@@ -0,0 +1 @@
1
+ export {};