@headless-lms/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/assets/events.d.ts +39 -21
  2. package/dist/assets/events.js +1 -1
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +5 -4
  8. package/dist/assets/index.js +3 -2
  9. package/dist/assets/model.d.ts +2 -2
  10. package/dist/assets/ports.d.ts +45 -15
  11. package/dist/assets/service.d.ts +39 -7
  12. package/dist/assets/service.js +291 -38
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.d.ts +13 -24
  15. package/dist/automations/actions.js +168 -43
  16. package/dist/automations/actions.js.map +1 -1
  17. package/dist/automations/catalog.d.ts +2 -1
  18. package/dist/automations/catalog.js +229 -13
  19. package/dist/automations/catalog.js.map +1 -1
  20. package/dist/automations/community-derivations.d.ts +14 -0
  21. package/dist/automations/community-derivations.js +439 -0
  22. package/dist/automations/community-derivations.js.map +1 -0
  23. package/dist/automations/community-mail.d.ts +112 -0
  24. package/dist/automations/community-mail.js +90 -0
  25. package/dist/automations/community-mail.js.map +1 -0
  26. package/dist/automations/community-notifications.d.ts +16 -0
  27. package/dist/automations/community-notifications.js +84 -0
  28. package/dist/automations/community-notifications.js.map +1 -0
  29. package/dist/automations/events.d.ts +28 -0
  30. package/dist/automations/index.d.ts +4 -2
  31. package/dist/automations/index.js +3 -2
  32. package/dist/automations/model.d.ts +10 -3
  33. package/dist/automations/model.js +15 -2
  34. package/dist/automations/model.js.map +1 -1
  35. package/dist/automations/ports.d.ts +5 -1
  36. package/dist/automations/schedule.d.ts +6 -0
  37. package/dist/automations/schedule.js +41 -0
  38. package/dist/automations/schedule.js.map +1 -0
  39. package/dist/automations/seeds.d.ts +17 -0
  40. package/dist/automations/seeds.js +235 -0
  41. package/dist/automations/seeds.js.map +1 -0
  42. package/dist/automations/service.d.ts +22 -3
  43. package/dist/automations/service.js +306 -43
  44. package/dist/automations/service.js.map +1 -1
  45. package/dist/content/events.d.ts +631 -19
  46. package/dist/content/events.js +44 -1
  47. package/dist/content/events.js.map +1 -1
  48. package/dist/content/index.d.ts +4 -4
  49. package/dist/content/model.d.ts +2 -2
  50. package/dist/content/ports.d.ts +71 -18
  51. package/dist/content/service.d.ts +57 -18
  52. package/dist/content/service.js +502 -23
  53. package/dist/content/service.js.map +1 -1
  54. package/dist/content/types.d.ts +2 -2
  55. package/dist/discussion/authors.d.ts +10 -0
  56. package/dist/discussion/authors.js +23 -0
  57. package/dist/discussion/authors.js.map +1 -0
  58. package/dist/discussion/chats.d.ts +104 -0
  59. package/dist/discussion/chats.js +798 -0
  60. package/dist/discussion/chats.js.map +1 -0
  61. package/dist/discussion/events.d.ts +619 -56
  62. package/dist/discussion/events.js +106 -3
  63. package/dist/discussion/events.js.map +1 -1
  64. package/dist/discussion/index.d.ts +7 -3
  65. package/dist/discussion/index.js +4 -1
  66. package/dist/discussion/ports.d.ts +288 -27
  67. package/dist/discussion/ports.js +6 -1
  68. package/dist/discussion/ports.js.map +1 -0
  69. package/dist/discussion/posts.d.ts +103 -0
  70. package/dist/discussion/posts.js +723 -0
  71. package/dist/discussion/posts.js.map +1 -0
  72. package/dist/discussion/service.d.ts +32 -10
  73. package/dist/discussion/service.js +293 -78
  74. package/dist/discussion/service.js.map +1 -1
  75. package/dist/entitlements/events.d.ts +178 -11
  76. package/dist/entitlements/events.js +28 -1
  77. package/dist/entitlements/events.js.map +1 -1
  78. package/dist/entitlements/index.d.ts +5 -4
  79. package/dist/entitlements/index.js +2 -1
  80. package/dist/entitlements/model.d.ts +3 -2
  81. package/dist/entitlements/ports.d.ts +124 -11
  82. package/dist/entitlements/rules.d.ts +12 -0
  83. package/dist/entitlements/rules.js +17 -0
  84. package/dist/entitlements/rules.js.map +1 -0
  85. package/dist/entitlements/service.d.ts +54 -3
  86. package/dist/entitlements/service.js +472 -2
  87. package/dist/entitlements/service.js.map +1 -1
  88. package/dist/identity/access.d.ts +6 -0
  89. package/dist/identity/access.js +7 -0
  90. package/dist/identity/access.js.map +1 -0
  91. package/dist/identity/index.d.ts +6 -4
  92. package/dist/identity/index.js +3 -1
  93. package/dist/identity/model.d.ts +2 -1
  94. package/dist/identity/notifications.d.ts +27 -0
  95. package/dist/identity/notifications.js +101 -0
  96. package/dist/identity/notifications.js.map +1 -0
  97. package/dist/identity/ports.d.ts +73 -3
  98. package/dist/identity/service.d.ts +14 -3
  99. package/dist/identity/service.js +62 -5
  100. package/dist/identity/service.js.map +1 -1
  101. package/dist/integrations/model.js +1 -1
  102. package/dist/integrations/model.js.map +1 -1
  103. package/dist/integrations/service.js +2 -2
  104. package/dist/integrations/service.js.map +1 -1
  105. package/dist/organizations/access-scopes.d.ts +19 -0
  106. package/dist/organizations/access-scopes.js +89 -0
  107. package/dist/organizations/access-scopes.js.map +1 -0
  108. package/dist/organizations/events.d.ts +74 -32
  109. package/dist/organizations/events.js +6 -0
  110. package/dist/organizations/events.js.map +1 -1
  111. package/dist/organizations/index.d.ts +7 -6
  112. package/dist/organizations/index.js +4 -3
  113. package/dist/organizations/members.d.ts +8 -5
  114. package/dist/organizations/members.js.map +1 -1
  115. package/dist/organizations/ports.d.ts +14 -11
  116. package/dist/organizations/roles.d.ts +6 -10
  117. package/dist/organizations/roles.js +19 -7
  118. package/dist/organizations/roles.js.map +1 -1
  119. package/dist/organizations/service.d.ts +20 -11
  120. package/dist/organizations/service.js +157 -47
  121. package/dist/organizations/service.js.map +1 -1
  122. package/dist/organizations/types.d.ts +1 -10
  123. package/dist/progress/index.d.ts +2 -2
  124. package/dist/progress/ports.d.ts +6 -1
  125. package/dist/progress/service.d.ts +3 -1
  126. package/dist/progress/service.js +6 -2
  127. package/dist/progress/service.js.map +1 -1
  128. package/dist/reporting/admin/index.d.ts +4 -0
  129. package/dist/reporting/admin/index.js +2 -0
  130. package/dist/reporting/admin/model.d.ts +108 -0
  131. package/dist/reporting/admin/ports.d.ts +27 -0
  132. package/dist/reporting/admin/service.d.ts +22 -0
  133. package/dist/reporting/admin/service.js +38 -0
  134. package/dist/reporting/admin/service.js.map +1 -0
  135. package/dist/reporting/member/index.d.ts +5 -0
  136. package/dist/reporting/member/index.js +2 -0
  137. package/dist/reporting/member/model.d.ts +124 -0
  138. package/dist/reporting/member/ports.d.ts +45 -0
  139. package/dist/reporting/member/service.d.ts +73 -0
  140. package/dist/reporting/member/service.js +286 -0
  141. package/dist/reporting/member/service.js.map +1 -0
  142. package/dist/shared/actor.d.ts +9 -0
  143. package/dist/shared/errors.d.ts +7 -1
  144. package/dist/shared/errors.js +10 -1
  145. package/dist/shared/errors.js.map +1 -1
  146. package/dist/shared/id.d.ts +11 -0
  147. package/dist/shared/id.js +12 -1
  148. package/dist/shared/id.js.map +1 -1
  149. package/dist/shared/mailer.d.ts +4 -1
  150. package/dist/shared/mailer.js.map +1 -1
  151. package/dist/shared/network.d.ts +10 -0
  152. package/dist/shared/network.js +211 -0
  153. package/dist/shared/network.js.map +1 -0
  154. package/dist/types/assets.d.ts +2 -2
  155. package/dist/types/automations.d.ts +10 -2
  156. package/dist/types/content.d.ts +2 -2
  157. package/dist/types/discussion.d.ts +11 -3
  158. package/dist/types/email-templates.d.ts +121 -0
  159. package/dist/types/entitlements.d.ts +2 -2
  160. package/dist/types/identity.d.ts +48 -2
  161. package/dist/types/index.d.ts +9 -9
  162. package/dist/types/ports.d.ts +6 -0
  163. package/dist/types/schemas/assets.d.ts +65 -37
  164. package/dist/types/schemas/assets.js +17 -12
  165. package/dist/types/schemas/assets.js.map +1 -1
  166. package/dist/types/schemas/automations.d.ts +18 -1
  167. package/dist/types/schemas/automations.js +15 -6
  168. package/dist/types/schemas/automations.js.map +1 -1
  169. package/dist/types/schemas/content.d.ts +335 -3
  170. package/dist/types/schemas/content.js +139 -3
  171. package/dist/types/schemas/content.js.map +1 -1
  172. package/dist/types/schemas/discussion.d.ts +370 -3
  173. package/dist/types/schemas/discussion.js +199 -4
  174. package/dist/types/schemas/discussion.js.map +1 -1
  175. package/dist/types/schemas/entitlements.d.ts +36 -2
  176. package/dist/types/schemas/entitlements.js +23 -1
  177. package/dist/types/schemas/entitlements.js.map +1 -1
  178. package/dist/types/schemas/identity.d.ts +111 -1
  179. package/dist/types/schemas/identity.js +55 -2
  180. package/dist/types/schemas/identity.js.map +1 -1
  181. package/dist/types/schemas/index.d.ts +8 -8
  182. package/dist/types/schemas/index.js +8 -8
  183. package/dist/types/schemas/organizations.d.ts +8 -0
  184. package/dist/types/schemas/organizations.js +2 -0
  185. package/dist/types/schemas/organizations.js.map +1 -1
  186. package/dist/types/schemas/shared.d.ts +3 -1
  187. package/dist/types/schemas/shared.js +5 -1
  188. package/dist/types/schemas/shared.js.map +1 -1
  189. package/package.json +2 -2
  190. package/dist/reporting/courses/index.d.ts +0 -4
  191. package/dist/reporting/courses/index.js +0 -2
  192. package/dist/reporting/courses/model.d.ts +0 -24
  193. package/dist/reporting/courses/ports.d.ts +0 -13
  194. package/dist/reporting/courses/service.d.ts +0 -19
  195. package/dist/reporting/courses/service.js +0 -20
  196. package/dist/reporting/courses/service.js.map +0 -1
  197. package/dist/reporting/dashboard/index.d.ts +0 -4
  198. package/dist/reporting/dashboard/index.js +0 -2
  199. package/dist/reporting/dashboard/model.d.ts +0 -14
  200. package/dist/reporting/dashboard/ports.d.ts +0 -13
  201. package/dist/reporting/dashboard/service.d.ts +0 -19
  202. package/dist/reporting/dashboard/service.js +0 -20
  203. package/dist/reporting/dashboard/service.js.map +0 -1
  204. package/dist/reporting/learn/index.d.ts +0 -5
  205. package/dist/reporting/learn/index.js +0 -2
  206. package/dist/reporting/learn/model.d.ts +0 -23
  207. package/dist/reporting/learn/ports.d.ts +0 -43
  208. package/dist/reporting/learn/ports.js +0 -1
  209. package/dist/reporting/learn/service.d.ts +0 -49
  210. package/dist/reporting/learn/service.js +0 -116
  211. package/dist/reporting/learn/service.js.map +0 -1
  212. package/dist/reporting/students/index.d.ts +0 -4
  213. package/dist/reporting/students/index.js +0 -2
  214. package/dist/reporting/students/model.d.ts +0 -51
  215. package/dist/reporting/students/model.js +0 -1
  216. package/dist/reporting/students/ports.d.ts +0 -15
  217. package/dist/reporting/students/ports.js +0 -1
  218. package/dist/reporting/students/service.d.ts +0 -20
  219. package/dist/reporting/students/service.js +0 -23
  220. package/dist/reporting/students/service.js.map +0 -1
  221. /package/dist/reporting/{courses → admin}/model.js +0 -0
  222. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  223. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  224. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  225. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
@@ -1,25 +1,67 @@
1
+ import { spaceSettingsDefaults } from "../types/schemas/content.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
1
3
  import { noopLogger } from "../shared/logger.js";
2
4
  import { contentEvents } from "./events.js";
3
- import { ConflictError, NotFoundError } from "../shared/errors.js";
4
5
  //#region src/content/service.ts
5
6
  /** URL-safe slug derived from a title. Domain rule owned by the service. */
6
7
  function slugify(title) {
7
8
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
8
9
  }
10
+ function actorMetadata(actor) {
11
+ return { actorOrgUserId: actor.id };
12
+ }
13
+ function assertConfigurator(actor) {
14
+ if (actor.role !== "owner" && actor.role !== "admin") throw new ForbiddenError("Only an owner or an admin can configure a community");
15
+ }
16
+ function sameIdSet(current, requested) {
17
+ if (current.length !== requested.length) return false;
18
+ const have = new Set(current);
19
+ return requested.every((id) => have.has(id)) && new Set(requested).size === requested.length;
20
+ }
9
21
  var ContentService = class {
10
22
  repo;
11
23
  uow;
24
+ libraryAssets;
25
+ files;
26
+ fileUrls;
12
27
  logger;
13
28
  constructor(params) {
14
29
  this.repo = params.repo;
15
30
  this.uow = params.uow;
31
+ this.libraryAssets = params.libraryAssets;
32
+ this.files = params.files;
33
+ this.fileUrls = params.fileUrls;
16
34
  this.logger = params.logger ?? noopLogger;
17
35
  }
18
- listCourses(orgId, query) {
19
- return this.repo.listCourses(orgId, query);
36
+ withThumbnail(item) {
37
+ return {
38
+ ...item,
39
+ thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.thumbnail(item.type, item.id) : null
40
+ };
20
41
  }
21
- getCourse(orgId, id) {
22
- return this.repo.findCourseById(orgId, id);
42
+ pageWithThumbnails(page) {
43
+ return {
44
+ ...page,
45
+ rows: page.rows.map((row) => this.withThumbnail(row))
46
+ };
47
+ }
48
+ async downloadFiles(orgId, links) {
49
+ const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
50
+ return links.flatMap((link) => {
51
+ const file = ready[link.assetId];
52
+ return file ? [{
53
+ ...link,
54
+ ...file,
55
+ url: this.fileUrls.downloadFile(link.downloadId, link.assetId)
56
+ }] : [];
57
+ });
58
+ }
59
+ async listCourses(orgId, query) {
60
+ return this.pageWithThumbnails(await this.repo.listCourses(orgId, query));
61
+ }
62
+ async getCourse(orgId, id) {
63
+ const course = await this.repo.findCourseById(orgId, id);
64
+ return course ? this.withThumbnail(course) : null;
23
65
  }
24
66
  async createCourse(orgId, input) {
25
67
  const course = await this.uow.run(async ({ content, outbox }) => {
@@ -35,9 +77,10 @@ var ContentService = class {
35
77
  courseId: course.id,
36
78
  title: course.title
37
79
  });
38
- return course;
80
+ return this.withThumbnail(course);
39
81
  }
40
82
  async updateCourse(orgId, id, patch) {
83
+ await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A course");
41
84
  const course = await this.uow.run(async ({ content, outbox }) => {
42
85
  const updated = await content.updateCourse(orgId, id, patch);
43
86
  if (!updated) {
@@ -58,7 +101,7 @@ var ContentService = class {
58
101
  courseId: id,
59
102
  fields: Object.keys(patch)
60
103
  });
61
- return course;
104
+ return this.withThumbnail(course);
62
105
  }
63
106
  async patchCourseSettings(orgId, id, value) {
64
107
  const merged = await this.uow.run(async ({ content, outbox }) => {
@@ -87,7 +130,7 @@ var ContentService = class {
87
130
  });
88
131
  return merged;
89
132
  }
90
- async deleteCourse(orgId, id) {
133
+ async deleteCourse(orgId, id, actor) {
91
134
  await this.uow.run(async ({ content, outbox }) => {
92
135
  const course = await content.findCourseById(orgId, id);
93
136
  if (!course) {
@@ -97,11 +140,12 @@ var ContentService = class {
97
140
  });
98
141
  throw new NotFoundError("Course", id);
99
142
  }
143
+ const linked = await content.spacesLinkingContent(orgId, id);
100
144
  if (!await content.deleteCourse(orgId, id)) throw new NotFoundError("Course", id);
101
145
  await outbox.append([contentEvents.courseDeleted.make({
102
146
  orgId,
103
147
  data: course
104
- })]);
148
+ }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
105
149
  });
106
150
  this.logger.info("course deleted", {
107
151
  orgId,
@@ -114,6 +158,13 @@ var ContentService = class {
114
158
  listCourseActivities(orgId, courseId) {
115
159
  return this.repo.listActivitiesForCourse(orgId, courseId);
116
160
  }
161
+ async listCourseActivityViews(orgId, courseId) {
162
+ const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
163
+ return activities.map((activity) => ({
164
+ ...activity,
165
+ assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
166
+ }));
167
+ }
117
168
  listCourseActivityAssets(orgId, courseId) {
118
169
  return this.repo.listActivityAssetsForCourse(orgId, courseId);
119
170
  }
@@ -232,7 +283,21 @@ var ContentService = class {
232
283
  });
233
284
  return modules;
234
285
  }
286
+ async assertLibraryAssets(orgId, assetIds, user = "An activity") {
287
+ const ids = assetIds.filter((id) => typeof id === "string");
288
+ if (ids.length === 0) return;
289
+ const library = new Set(await this.libraryAssets.libraryAssetsAmong(orgId, ids));
290
+ const refused = ids.filter((id) => !library.has(id));
291
+ if (refused.length > 0) {
292
+ this.logger.warn("library assets refused", {
293
+ orgId,
294
+ assetIds: refused
295
+ });
296
+ throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${refused.join(", ")}`);
297
+ }
298
+ }
235
299
  async saveActivity(orgId, courseId, moduleId, input, activityId) {
300
+ await this.assertLibraryAssets(orgId, input.assetIds ?? []);
236
301
  const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
237
302
  if (activityId) {
238
303
  const existing = await content.findActivity(orgId, activityId);
@@ -304,11 +369,12 @@ var ContentService = class {
304
369
  });
305
370
  return modules;
306
371
  }
307
- listDownloads(orgId, query) {
308
- return this.repo.listDownloads(orgId, query);
372
+ async listDownloads(orgId, query) {
373
+ return this.pageWithThumbnails(await this.repo.listDownloads(orgId, query));
309
374
  }
310
- getDownload(orgId, id) {
311
- return this.repo.getDownload(orgId, id);
375
+ async getDownload(orgId, id) {
376
+ const download = await this.repo.getDownload(orgId, id);
377
+ return download ? this.withThumbnail(download) : null;
312
378
  }
313
379
  async createDownload(orgId, input) {
314
380
  const download = await this.uow.run(async ({ content, outbox }) => {
@@ -324,9 +390,10 @@ var ContentService = class {
324
390
  downloadId: download.id,
325
391
  title: download.title
326
392
  });
327
- return download;
393
+ return this.withThumbnail(download);
328
394
  }
329
395
  async updateDownload(orgId, id, patch) {
396
+ await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A download");
330
397
  const download = await this.uow.run(async ({ content, outbox }) => {
331
398
  const updated = await content.updateDownload(orgId, id, patch);
332
399
  if (!updated) {
@@ -347,9 +414,9 @@ var ContentService = class {
347
414
  downloadId: id,
348
415
  fields: Object.keys(patch)
349
416
  });
350
- return download;
417
+ return this.withThumbnail(download);
351
418
  }
352
- async removeDownload(orgId, id) {
419
+ async removeDownload(orgId, id, actor) {
353
420
  await this.uow.run(async ({ content, outbox }) => {
354
421
  const download = await content.getDownload(orgId, id);
355
422
  if (!download) {
@@ -359,21 +426,23 @@ var ContentService = class {
359
426
  });
360
427
  throw new NotFoundError("Download", id);
361
428
  }
429
+ const linked = await content.spacesLinkingContent(orgId, id);
362
430
  if (!await content.deleteDownload(orgId, id)) throw new NotFoundError("Download", id);
363
431
  await outbox.append([contentEvents.downloadDeleted.make({
364
432
  orgId,
365
433
  data: download
366
- })]);
434
+ }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
367
435
  });
368
436
  this.logger.info("download deleted", {
369
437
  orgId,
370
438
  downloadId: id
371
439
  });
372
440
  }
373
- listDownloadAssets(orgId, downloadId) {
374
- return this.repo.listDownloadAssets(orgId, downloadId);
441
+ async listDownloadAssets(orgId, downloadId) {
442
+ return this.downloadFiles(orgId, await this.repo.listDownloadAssets(orgId, downloadId));
375
443
  }
376
444
  async addDownloadAsset(orgId, downloadId, input) {
445
+ await this.assertLibraryAssets(orgId, [input.assetId], "A download");
377
446
  const assets = await this.uow.run(async ({ content, outbox }) => {
378
447
  const assets = await content.addDownloadAsset(orgId, downloadId, input);
379
448
  await outbox.append([contentEvents.downloadAssetAdded.make({
@@ -390,7 +459,7 @@ var ContentService = class {
390
459
  downloadId,
391
460
  assetId: input.assetId
392
461
  });
393
- return assets;
462
+ return this.downloadFiles(orgId, assets);
394
463
  }
395
464
  async removeDownloadAsset(orgId, downloadId, assetId) {
396
465
  const assets = await this.uow.run(async ({ content, outbox }) => {
@@ -409,7 +478,7 @@ var ContentService = class {
409
478
  downloadId,
410
479
  assetId
411
480
  });
412
- return assets;
481
+ return this.downloadFiles(orgId, assets);
413
482
  }
414
483
  async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
415
484
  const assets = await this.uow.run(async ({ content, outbox }) => {
@@ -428,7 +497,7 @@ var ContentService = class {
428
497
  downloadId,
429
498
  assetId
430
499
  });
431
- return assets;
500
+ return this.downloadFiles(orgId, assets);
432
501
  }
433
502
  /** The caller must send the COMPLETE ordered set — a partial list would
434
503
  * silently unlink whatever it omitted. */
@@ -460,7 +529,7 @@ var ContentService = class {
460
529
  orgId,
461
530
  downloadId
462
531
  });
463
- return assets;
532
+ return this.downloadFiles(orgId, assets);
464
533
  }
465
534
  listBundles(orgId, query) {
466
535
  return this.repo.listBundles(orgId, query);
@@ -580,6 +649,416 @@ var ContentService = class {
580
649
  });
581
650
  return items;
582
651
  }
652
+ async spaceUpdates(content, orgId, spaceIds, actor) {
653
+ const events = [];
654
+ for (const spaceId of spaceIds) {
655
+ const space = await content.findSpace(orgId, spaceId);
656
+ if (space) events.push(contentEvents.spaceUpdated.make({
657
+ orgId,
658
+ data: space,
659
+ metadata: actorMetadata(actor)
660
+ }));
661
+ }
662
+ return events;
663
+ }
664
+ resolvedSpace(input, current) {
665
+ const accessRule = input.accessRule ?? current?.accessRule ?? "all_members";
666
+ let capacity = input.capacity !== void 0 ? input.capacity : current?.capacity ?? null;
667
+ if (accessRule === "all_members") {
668
+ if (input.capacity != null) throw new InvalidInputError("An all-members space has no capacity");
669
+ capacity = null;
670
+ }
671
+ const settings = {
672
+ ...current?.settings ?? spaceSettingsDefaults,
673
+ ...input.settings
674
+ };
675
+ return {
676
+ accessRule,
677
+ capacity,
678
+ settings
679
+ };
680
+ }
681
+ async listCommunities(orgId, query) {
682
+ return this.pageWithThumbnails(await this.repo.listCommunities(orgId, query));
683
+ }
684
+ async getCommunity(orgId, id) {
685
+ const community = await this.repo.findCommunityById(orgId, id);
686
+ return community ? this.withThumbnail(community) : null;
687
+ }
688
+ listPublishedCommunityIds(orgId) {
689
+ return this.repo.listPublishedCommunityIds(orgId);
690
+ }
691
+ listCommunitiesById(orgId, ids) {
692
+ return ids.length ? this.repo.findCommunitiesByIds(orgId, ids) : Promise.resolve([]);
693
+ }
694
+ getSpace(orgId, spaceId) {
695
+ return this.repo.findSpace(orgId, spaceId);
696
+ }
697
+ async getCommunitySpace(orgId, communityId, spaceId) {
698
+ const space = await this.repo.findSpace(orgId, spaceId);
699
+ return space?.communityId === communityId ? space : null;
700
+ }
701
+ async listSpaces(orgId, communityId) {
702
+ if (!await this.getCommunity(orgId, communityId)) throw new NotFoundError("Community", communityId);
703
+ return this.repo.listSpaces(orgId, communityId);
704
+ }
705
+ async getDmSettings(orgId, communityId) {
706
+ return (await this.repo.findCommunityById(orgId, communityId))?.settings.dm ?? null;
707
+ }
708
+ async createCommunity(orgId, input, actor) {
709
+ assertConfigurator(actor);
710
+ const community = await this.uow.run(async ({ content, outbox }) => {
711
+ const created = await content.createCommunity(orgId, input, slugify(input.title));
712
+ await outbox.append([contentEvents.communityCreated.make({
713
+ orgId,
714
+ data: created,
715
+ metadata: actorMetadata(actor)
716
+ })]);
717
+ return created;
718
+ });
719
+ this.logger.info("community created", {
720
+ orgId,
721
+ communityId: community.id,
722
+ actorId: actor.id
723
+ });
724
+ return this.withThumbnail(community);
725
+ }
726
+ async updateCommunity(orgId, id, patch, actor) {
727
+ assertConfigurator(actor);
728
+ await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A community");
729
+ const community = await this.uow.run(async ({ content, outbox }) => {
730
+ const updated = await content.updateCommunity(orgId, id, patch);
731
+ if (!updated) {
732
+ this.logger.warn("community update rejected — not found", {
733
+ orgId,
734
+ communityId: id,
735
+ actorId: actor.id
736
+ });
737
+ throw new NotFoundError("Community", id);
738
+ }
739
+ await outbox.append([contentEvents.communityUpdated.make({
740
+ orgId,
741
+ data: updated,
742
+ metadata: actorMetadata(actor)
743
+ })]);
744
+ return updated;
745
+ });
746
+ this.logger.info("community updated", {
747
+ orgId,
748
+ communityId: id,
749
+ actorId: actor.id
750
+ });
751
+ return this.withThumbnail(community);
752
+ }
753
+ publishCommunity(orgId, id, actor) {
754
+ return this.updateCommunity(orgId, id, { status: "published" }, actor);
755
+ }
756
+ unpublishCommunity(orgId, id, actor) {
757
+ return this.updateCommunity(orgId, id, { status: "draft" }, actor);
758
+ }
759
+ async deleteCommunity(orgId, id, actor) {
760
+ assertConfigurator(actor);
761
+ await this.uow.run(async ({ content, outbox }) => {
762
+ const community = await content.findCommunityById(orgId, id);
763
+ if (!community) {
764
+ this.logger.warn("community delete rejected — not found", {
765
+ orgId,
766
+ communityId: id,
767
+ actorId: actor.id
768
+ });
769
+ throw new NotFoundError("Community", id);
770
+ }
771
+ const linked = await content.spacesLinkingContent(orgId, id);
772
+ if (!await content.deleteCommunity(orgId, id)) throw new NotFoundError("Community", id);
773
+ await content.deleteCommunitySettings(orgId, id);
774
+ await outbox.append([contentEvents.communityDeleted.make({
775
+ orgId,
776
+ data: community,
777
+ metadata: actorMetadata(actor)
778
+ }), ...await this.spaceUpdates(content, orgId, linked, actor)]);
779
+ });
780
+ this.logger.info("community deleted", {
781
+ orgId,
782
+ communityId: id,
783
+ actorId: actor.id
784
+ });
785
+ }
786
+ async patchDmSettings(orgId, id, patch, actor) {
787
+ assertConfigurator(actor);
788
+ const community = await this.uow.run(async ({ content, outbox }) => {
789
+ const updated = await content.patchDmSettings(orgId, id, patch);
790
+ if (!updated) {
791
+ this.logger.warn("community dm settings patch rejected — not found", {
792
+ orgId,
793
+ communityId: id,
794
+ actorId: actor.id
795
+ });
796
+ throw new NotFoundError("Community", id);
797
+ }
798
+ await outbox.append([contentEvents.communityUpdated.make({
799
+ orgId,
800
+ data: updated,
801
+ metadata: actorMetadata(actor)
802
+ })]);
803
+ return updated;
804
+ });
805
+ this.logger.info("community dm settings patched", {
806
+ orgId,
807
+ communityId: id,
808
+ actorId: actor.id
809
+ });
810
+ return this.withThumbnail(community);
811
+ }
812
+ async createSpace(orgId, communityId, input, actor) {
813
+ assertConfigurator(actor);
814
+ const resolved = this.resolvedSpace(input);
815
+ const space = await this.uow.run(async ({ content, outbox }) => {
816
+ if (!await content.findCommunityById(orgId, communityId)) {
817
+ this.logger.warn("space create rejected — community not found", {
818
+ orgId,
819
+ communityId,
820
+ actorId: actor.id
821
+ });
822
+ throw new NotFoundError("Community", communityId);
823
+ }
824
+ const created = await content.createSpace(orgId, communityId, {
825
+ ...input,
826
+ accessRule: resolved.accessRule,
827
+ capacity: resolved.capacity,
828
+ settings: resolved.settings
829
+ });
830
+ await outbox.append([contentEvents.spaceCreated.make({
831
+ orgId,
832
+ data: created,
833
+ metadata: actorMetadata(actor)
834
+ })]);
835
+ return created;
836
+ });
837
+ this.logger.info("space created", {
838
+ orgId,
839
+ communityId,
840
+ spaceId: space.id,
841
+ actorId: actor.id
842
+ });
843
+ return space;
844
+ }
845
+ async updateSpace(orgId, communityId, spaceId, patch, actor) {
846
+ assertConfigurator(actor);
847
+ const space = await this.uow.run(async ({ content, outbox }) => {
848
+ const current = await content.findSpace(orgId, spaceId);
849
+ if (!current || current.communityId !== communityId) {
850
+ this.logger.warn("space update rejected — not found", {
851
+ orgId,
852
+ communityId,
853
+ spaceId,
854
+ actorId: actor.id
855
+ });
856
+ throw new NotFoundError("Space", spaceId);
857
+ }
858
+ const resolved = this.resolvedSpace(patch, current);
859
+ const updated = await content.updateSpace(orgId, communityId, spaceId, {
860
+ ...patch,
861
+ accessRule: resolved.accessRule,
862
+ capacity: resolved.capacity,
863
+ settings: resolved.settings
864
+ });
865
+ if (!updated) throw new NotFoundError("Space", spaceId);
866
+ await outbox.append([contentEvents.spaceUpdated.make({
867
+ orgId,
868
+ data: updated,
869
+ metadata: actorMetadata(actor)
870
+ })]);
871
+ return updated;
872
+ });
873
+ this.logger.info("space updated", {
874
+ orgId,
875
+ communityId,
876
+ spaceId,
877
+ actorId: actor.id
878
+ });
879
+ return space;
880
+ }
881
+ async deleteSpace(orgId, communityId, spaceId, actor) {
882
+ assertConfigurator(actor);
883
+ await this.uow.run(async ({ content, outbox }) => {
884
+ const space = await content.findSpace(orgId, spaceId);
885
+ if (!space || space.communityId !== communityId) {
886
+ this.logger.warn("space delete rejected — not found", {
887
+ orgId,
888
+ communityId,
889
+ spaceId,
890
+ actorId: actor.id
891
+ });
892
+ throw new NotFoundError("Space", spaceId);
893
+ }
894
+ if (!await content.deleteSpace(orgId, communityId, spaceId)) throw new NotFoundError("Space", spaceId);
895
+ await outbox.append([contentEvents.spaceDeleted.make({
896
+ orgId,
897
+ data: space,
898
+ metadata: actorMetadata(actor)
899
+ })]);
900
+ });
901
+ this.logger.info("space deleted", {
902
+ orgId,
903
+ communityId,
904
+ spaceId,
905
+ actorId: actor.id
906
+ });
907
+ }
908
+ async reorderSpaces(orgId, communityId, orderedIds, actor) {
909
+ assertConfigurator(actor);
910
+ const spaces = await this.uow.run(async ({ content, outbox }) => {
911
+ if (!await content.findCommunityById(orgId, communityId)) {
912
+ this.logger.warn("spaces reorder rejected — community not found", {
913
+ orgId,
914
+ communityId,
915
+ actorId: actor.id
916
+ });
917
+ throw new NotFoundError("Community", communityId);
918
+ }
919
+ if (!sameIdSet((await content.listSpaces(orgId, communityId)).map((space) => space.id), orderedIds)) throw new ConflictError("Ordered space ids do not match the community's spaces");
920
+ const reordered = await content.reorderSpaces(orgId, communityId, orderedIds);
921
+ await outbox.append([contentEvents.spacesReordered.make({
922
+ orgId,
923
+ data: reordered,
924
+ metadata: actorMetadata(actor)
925
+ })]);
926
+ return reordered;
927
+ });
928
+ this.logger.info("spaces reordered", {
929
+ orgId,
930
+ communityId,
931
+ actorId: actor.id
932
+ });
933
+ return spaces;
934
+ }
935
+ async setSpaceTopics(orgId, communityId, spaceId, input, actor) {
936
+ assertConfigurator(actor);
937
+ const names = input.topics.map((topic) => topic.name);
938
+ if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
939
+ const space = await this.uow.run(async ({ content, outbox }) => {
940
+ const locked = await content.lockSpace(orgId, spaceId);
941
+ if (!locked || locked.communityId !== communityId) {
942
+ this.logger.warn("space topics rejected — space not found", {
943
+ orgId,
944
+ communityId,
945
+ spaceId,
946
+ actorId: actor.id
947
+ });
948
+ throw new NotFoundError("Space", spaceId);
949
+ }
950
+ const current = await content.findSpace(orgId, spaceId);
951
+ if (!current) throw new NotFoundError("Space", spaceId);
952
+ const kept = new Set(input.topics.flatMap((topic) => topic.id ? [topic.id] : []));
953
+ const removed = current.topics.filter((topic) => !kept.has(topic.id)).map((topic) => topic.id);
954
+ if (removed.length > 0 && (await content.topicsInUse(orgId, spaceId, removed)).length > 0) throw new ConflictError("A topic used by a post cannot be removed");
955
+ const updated = await content.setSpaceTopics(orgId, spaceId, input);
956
+ await outbox.append([contentEvents.spaceUpdated.make({
957
+ orgId,
958
+ data: updated,
959
+ metadata: actorMetadata(actor)
960
+ })]);
961
+ return updated;
962
+ });
963
+ this.logger.info("space topics set", {
964
+ orgId,
965
+ communityId,
966
+ spaceId,
967
+ actorId: actor.id
968
+ });
969
+ return space;
970
+ }
971
+ async addSpaceLink(orgId, communityId, spaceId, contentId, actor) {
972
+ assertConfigurator(actor);
973
+ const space = await this.uow.run(async ({ content, outbox }) => {
974
+ const current = await content.findSpace(orgId, spaceId);
975
+ if (!current || current.communityId !== communityId) {
976
+ this.logger.warn("space link add rejected — space not found", {
977
+ orgId,
978
+ communityId,
979
+ spaceId,
980
+ actorId: actor.id
981
+ });
982
+ throw new NotFoundError("Space", spaceId);
983
+ }
984
+ if (current.links.some((link) => link.contentId === contentId)) throw new ConflictError("This space already links to that content");
985
+ const updated = await content.addSpaceLink(orgId, spaceId, contentId);
986
+ await outbox.append([contentEvents.spaceUpdated.make({
987
+ orgId,
988
+ data: updated,
989
+ metadata: actorMetadata(actor)
990
+ })]);
991
+ return updated;
992
+ });
993
+ this.logger.info("space link added", {
994
+ orgId,
995
+ communityId,
996
+ spaceId,
997
+ contentId,
998
+ actorId: actor.id
999
+ });
1000
+ return space;
1001
+ }
1002
+ async removeSpaceLink(orgId, communityId, spaceId, linkId, actor) {
1003
+ assertConfigurator(actor);
1004
+ const space = await this.uow.run(async ({ content, outbox }) => {
1005
+ const current = await content.findSpace(orgId, spaceId);
1006
+ if (!current || current.communityId !== communityId) {
1007
+ this.logger.warn("space link remove rejected — space not found", {
1008
+ orgId,
1009
+ communityId,
1010
+ spaceId,
1011
+ actorId: actor.id
1012
+ });
1013
+ throw new NotFoundError("Space", spaceId);
1014
+ }
1015
+ const updated = await content.removeSpaceLink(orgId, spaceId, linkId);
1016
+ await outbox.append([contentEvents.spaceUpdated.make({
1017
+ orgId,
1018
+ data: updated,
1019
+ metadata: actorMetadata(actor)
1020
+ })]);
1021
+ return updated;
1022
+ });
1023
+ this.logger.info("space link removed", {
1024
+ orgId,
1025
+ communityId,
1026
+ spaceId,
1027
+ linkId,
1028
+ actorId: actor.id
1029
+ });
1030
+ return space;
1031
+ }
1032
+ async reorderSpaceLinks(orgId, communityId, spaceId, orderedIds, actor) {
1033
+ assertConfigurator(actor);
1034
+ const space = await this.uow.run(async ({ content, outbox }) => {
1035
+ const current = await content.findSpace(orgId, spaceId);
1036
+ if (!current || current.communityId !== communityId) {
1037
+ this.logger.warn("space links reorder rejected — space not found", {
1038
+ orgId,
1039
+ communityId,
1040
+ spaceId,
1041
+ actorId: actor.id
1042
+ });
1043
+ throw new NotFoundError("Space", spaceId);
1044
+ }
1045
+ if (!sameIdSet(current.links.map((link) => link.id), orderedIds)) throw new ConflictError("Ordered link ids do not match the space's links");
1046
+ const updated = await content.reorderSpaceLinks(orgId, spaceId, orderedIds);
1047
+ await outbox.append([contentEvents.spaceUpdated.make({
1048
+ orgId,
1049
+ data: updated,
1050
+ metadata: actorMetadata(actor)
1051
+ })]);
1052
+ return updated;
1053
+ });
1054
+ this.logger.info("space links reordered", {
1055
+ orgId,
1056
+ communityId,
1057
+ spaceId,
1058
+ actorId: actor.id
1059
+ });
1060
+ return space;
1061
+ }
583
1062
  };
584
1063
  //#endregion
585
1064
  export { ContentService };