@headless-lms/core 0.2.0 → 0.4.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.
- package/dist/assets/events.d.ts +42 -24
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +6 -4
- package/dist/assets/index.js +4 -2
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +46 -15
- package/dist/assets/service.d.ts +38 -7
- package/dist/assets/service.js +287 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +106 -42
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +226 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +98 -0
- package/dist/automations/community-mail.js +70 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/events.d.ts +18 -0
- package/dist/automations/index.d.ts +13 -4
- package/dist/automations/index.js +10 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +121 -3
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/service.d.ts +20 -4
- package/dist/automations/service.js +246 -45
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +712 -50
- package/dist/content/events.js +55 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +90 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +520 -25
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +109 -0
- package/dist/discussion/chats.js +831 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +785 -56
- package/dist/discussion/events.js +165 -5
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -3
- package/dist/discussion/index.js +9 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +372 -28
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +113 -0
- package/dist/discussion/posts.js +775 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +42 -11
- package/dist/discussion/service.js +364 -85
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +6 -4
- package/dist/entitlements/index.js +3 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +128 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +57 -3
- package/dist/entitlements/service.js +492 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +7 -4
- package/dist/identity/index.js +4 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +97 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +75 -33
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -6
- package/dist/organizations/index.js +5 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +17 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +22 -11
- package/dist/organizations/service.js +164 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +157 -0
- package/dist/reporting/member/ports.d.ts +48 -0
- package/dist/reporting/member/service.d.ts +92 -0
- package/dist/reporting/member/service.js +364 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +19 -1
- package/dist/shared/errors.js +27 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +14 -0
- package/dist/shared/id.js +15 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +15 -2
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +15 -1
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +216 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -8
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +9 -1
- package/dist/types/schemas/automations.js +13 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +396 -3
- package/dist/types/schemas/content.js +161 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +443 -3
- package/dist/types/schemas/discussion.js +233 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +27 -1
- package/dist/types/schemas/identity.js +14 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -8
- package/dist/types/schemas/index.js +9 -8
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -3
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
- /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
package/dist/content/service.js
CHANGED
|
@@ -1,25 +1,70 @@
|
|
|
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 isPublishedActivity(settings) {
|
|
11
|
+
return settings?.published !== false;
|
|
12
|
+
}
|
|
13
|
+
function actorMetadata(actor) {
|
|
14
|
+
return { actorOrgUserId: actor.id };
|
|
15
|
+
}
|
|
16
|
+
function assertConfigurator(actor) {
|
|
17
|
+
if (actor.role !== "owner" && actor.role !== "admin") throw new ForbiddenError("Only an owner or an admin can configure a community");
|
|
18
|
+
}
|
|
19
|
+
function sameIdSet(current, requested) {
|
|
20
|
+
if (current.length !== requested.length) return false;
|
|
21
|
+
const have = new Set(current);
|
|
22
|
+
return requested.every((id) => have.has(id)) && new Set(requested).size === requested.length;
|
|
23
|
+
}
|
|
9
24
|
var ContentService = class {
|
|
10
25
|
repo;
|
|
11
26
|
uow;
|
|
27
|
+
libraryAssets;
|
|
28
|
+
files;
|
|
29
|
+
fileUrls;
|
|
12
30
|
logger;
|
|
13
31
|
constructor(params) {
|
|
14
32
|
this.repo = params.repo;
|
|
15
33
|
this.uow = params.uow;
|
|
34
|
+
this.libraryAssets = params.libraryAssets;
|
|
35
|
+
this.files = params.files;
|
|
36
|
+
this.fileUrls = params.fileUrls;
|
|
16
37
|
this.logger = params.logger ?? noopLogger;
|
|
17
38
|
}
|
|
18
|
-
|
|
19
|
-
return
|
|
39
|
+
withThumbnail(item) {
|
|
40
|
+
return {
|
|
41
|
+
...item,
|
|
42
|
+
thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.thumbnail(item.type, item.id) : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
pageWithThumbnails(page) {
|
|
46
|
+
return {
|
|
47
|
+
...page,
|
|
48
|
+
rows: page.rows.map((row) => this.withThumbnail(row))
|
|
49
|
+
};
|
|
20
50
|
}
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
async downloadFiles(orgId, links) {
|
|
52
|
+
const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
|
|
53
|
+
return links.flatMap((link) => {
|
|
54
|
+
const file = ready[link.assetId];
|
|
55
|
+
return file ? [{
|
|
56
|
+
...link,
|
|
57
|
+
...file,
|
|
58
|
+
url: this.fileUrls.downloadFile(link.downloadId, link.assetId)
|
|
59
|
+
}] : [];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async listCourses(orgId, query) {
|
|
63
|
+
return this.pageWithThumbnails(await this.repo.listCourses(orgId, query));
|
|
64
|
+
}
|
|
65
|
+
async getCourse(orgId, id) {
|
|
66
|
+
const course = await this.repo.findCourseById(orgId, id);
|
|
67
|
+
return course ? this.withThumbnail(course) : null;
|
|
23
68
|
}
|
|
24
69
|
async createCourse(orgId, input) {
|
|
25
70
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -35,9 +80,10 @@ var ContentService = class {
|
|
|
35
80
|
courseId: course.id,
|
|
36
81
|
title: course.title
|
|
37
82
|
});
|
|
38
|
-
return course;
|
|
83
|
+
return this.withThumbnail(course);
|
|
39
84
|
}
|
|
40
85
|
async updateCourse(orgId, id, patch) {
|
|
86
|
+
await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A course");
|
|
41
87
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
42
88
|
const updated = await content.updateCourse(orgId, id, patch);
|
|
43
89
|
if (!updated) {
|
|
@@ -58,7 +104,7 @@ var ContentService = class {
|
|
|
58
104
|
courseId: id,
|
|
59
105
|
fields: Object.keys(patch)
|
|
60
106
|
});
|
|
61
|
-
return course;
|
|
107
|
+
return this.withThumbnail(course);
|
|
62
108
|
}
|
|
63
109
|
async patchCourseSettings(orgId, id, value) {
|
|
64
110
|
const merged = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -87,7 +133,7 @@ var ContentService = class {
|
|
|
87
133
|
});
|
|
88
134
|
return merged;
|
|
89
135
|
}
|
|
90
|
-
async deleteCourse(orgId, id) {
|
|
136
|
+
async deleteCourse(orgId, id, actor) {
|
|
91
137
|
await this.uow.run(async ({ content, outbox }) => {
|
|
92
138
|
const course = await content.findCourseById(orgId, id);
|
|
93
139
|
if (!course) {
|
|
@@ -97,11 +143,12 @@ var ContentService = class {
|
|
|
97
143
|
});
|
|
98
144
|
throw new NotFoundError("Course", id);
|
|
99
145
|
}
|
|
146
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
100
147
|
if (!await content.deleteCourse(orgId, id)) throw new NotFoundError("Course", id);
|
|
101
148
|
await outbox.append([contentEvents.courseDeleted.make({
|
|
102
149
|
orgId,
|
|
103
150
|
data: course
|
|
104
|
-
})]);
|
|
151
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
105
152
|
});
|
|
106
153
|
this.logger.info("course deleted", {
|
|
107
154
|
orgId,
|
|
@@ -114,6 +161,13 @@ var ContentService = class {
|
|
|
114
161
|
listCourseActivities(orgId, courseId) {
|
|
115
162
|
return this.repo.listActivitiesForCourse(orgId, courseId);
|
|
116
163
|
}
|
|
164
|
+
async listCourseActivityViews(orgId, courseId) {
|
|
165
|
+
const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
|
|
166
|
+
return activities.map((activity) => ({
|
|
167
|
+
...activity,
|
|
168
|
+
assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
117
171
|
listCourseActivityAssets(orgId, courseId) {
|
|
118
172
|
return this.repo.listActivityAssetsForCourse(orgId, courseId);
|
|
119
173
|
}
|
|
@@ -232,8 +286,23 @@ var ContentService = class {
|
|
|
232
286
|
});
|
|
233
287
|
return modules;
|
|
234
288
|
}
|
|
289
|
+
async assertLibraryAssets(orgId, assetIds, user = "An activity") {
|
|
290
|
+
const ids = assetIds.filter((id) => typeof id === "string");
|
|
291
|
+
if (ids.length === 0) return;
|
|
292
|
+
const library = new Set(await this.libraryAssets.libraryAssetsAmong(orgId, ids));
|
|
293
|
+
const refused = ids.filter((id) => !library.has(id));
|
|
294
|
+
if (refused.length > 0) {
|
|
295
|
+
this.logger.warn("library assets refused", {
|
|
296
|
+
orgId,
|
|
297
|
+
assetIds: refused
|
|
298
|
+
});
|
|
299
|
+
throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${refused.join(", ")}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
235
302
|
async saveActivity(orgId, courseId, moduleId, input, activityId) {
|
|
303
|
+
await this.assertLibraryAssets(orgId, input.assetIds ?? []);
|
|
236
304
|
const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
|
|
305
|
+
let wasPublished = false;
|
|
237
306
|
if (activityId) {
|
|
238
307
|
const existing = await content.findActivity(orgId, activityId);
|
|
239
308
|
if (!existing || existing.moduleId !== moduleId || existing.courseId !== courseId) {
|
|
@@ -245,6 +314,7 @@ var ContentService = class {
|
|
|
245
314
|
});
|
|
246
315
|
throw new NotFoundError("Activity", activityId);
|
|
247
316
|
}
|
|
317
|
+
wasPublished = isPublishedActivity(existing.settings);
|
|
248
318
|
} else {
|
|
249
319
|
const module = await content.findModule(orgId, moduleId);
|
|
250
320
|
if (!module || module.courseId !== courseId) {
|
|
@@ -257,13 +327,24 @@ var ContentService = class {
|
|
|
257
327
|
}
|
|
258
328
|
}
|
|
259
329
|
const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
|
|
260
|
-
|
|
330
|
+
const events = [activityId ? contentEvents.activityUpdated.make({
|
|
261
331
|
orgId,
|
|
262
332
|
data: saved
|
|
263
333
|
}) : contentEvents.activityCreated.make({
|
|
264
334
|
orgId,
|
|
265
335
|
data: saved
|
|
266
|
-
})]
|
|
336
|
+
})];
|
|
337
|
+
if (!wasPublished && isPublishedActivity(saved.settings)) {
|
|
338
|
+
if ((await content.findCourseById(orgId, courseId))?.status === "published") events.push(contentEvents.activityPublished.make({
|
|
339
|
+
orgId,
|
|
340
|
+
data: {
|
|
341
|
+
activityId: saved.id,
|
|
342
|
+
moduleId,
|
|
343
|
+
courseId
|
|
344
|
+
}
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
await outbox.append(events);
|
|
267
348
|
return {
|
|
268
349
|
modules: all,
|
|
269
350
|
activity: saved
|
|
@@ -304,11 +385,12 @@ var ContentService = class {
|
|
|
304
385
|
});
|
|
305
386
|
return modules;
|
|
306
387
|
}
|
|
307
|
-
listDownloads(orgId, query) {
|
|
308
|
-
return this.repo.listDownloads(orgId, query);
|
|
388
|
+
async listDownloads(orgId, query) {
|
|
389
|
+
return this.pageWithThumbnails(await this.repo.listDownloads(orgId, query));
|
|
309
390
|
}
|
|
310
|
-
getDownload(orgId, id) {
|
|
311
|
-
|
|
391
|
+
async getDownload(orgId, id) {
|
|
392
|
+
const download = await this.repo.getDownload(orgId, id);
|
|
393
|
+
return download ? this.withThumbnail(download) : null;
|
|
312
394
|
}
|
|
313
395
|
async createDownload(orgId, input) {
|
|
314
396
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -324,9 +406,10 @@ var ContentService = class {
|
|
|
324
406
|
downloadId: download.id,
|
|
325
407
|
title: download.title
|
|
326
408
|
});
|
|
327
|
-
return download;
|
|
409
|
+
return this.withThumbnail(download);
|
|
328
410
|
}
|
|
329
411
|
async updateDownload(orgId, id, patch) {
|
|
412
|
+
await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A download");
|
|
330
413
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
331
414
|
const updated = await content.updateDownload(orgId, id, patch);
|
|
332
415
|
if (!updated) {
|
|
@@ -347,9 +430,9 @@ var ContentService = class {
|
|
|
347
430
|
downloadId: id,
|
|
348
431
|
fields: Object.keys(patch)
|
|
349
432
|
});
|
|
350
|
-
return download;
|
|
433
|
+
return this.withThumbnail(download);
|
|
351
434
|
}
|
|
352
|
-
async removeDownload(orgId, id) {
|
|
435
|
+
async removeDownload(orgId, id, actor) {
|
|
353
436
|
await this.uow.run(async ({ content, outbox }) => {
|
|
354
437
|
const download = await content.getDownload(orgId, id);
|
|
355
438
|
if (!download) {
|
|
@@ -359,21 +442,23 @@ var ContentService = class {
|
|
|
359
442
|
});
|
|
360
443
|
throw new NotFoundError("Download", id);
|
|
361
444
|
}
|
|
445
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
362
446
|
if (!await content.deleteDownload(orgId, id)) throw new NotFoundError("Download", id);
|
|
363
447
|
await outbox.append([contentEvents.downloadDeleted.make({
|
|
364
448
|
orgId,
|
|
365
449
|
data: download
|
|
366
|
-
})]);
|
|
450
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
367
451
|
});
|
|
368
452
|
this.logger.info("download deleted", {
|
|
369
453
|
orgId,
|
|
370
454
|
downloadId: id
|
|
371
455
|
});
|
|
372
456
|
}
|
|
373
|
-
listDownloadAssets(orgId, downloadId) {
|
|
374
|
-
return this.repo.listDownloadAssets(orgId, downloadId);
|
|
457
|
+
async listDownloadAssets(orgId, downloadId) {
|
|
458
|
+
return this.downloadFiles(orgId, await this.repo.listDownloadAssets(orgId, downloadId));
|
|
375
459
|
}
|
|
376
460
|
async addDownloadAsset(orgId, downloadId, input) {
|
|
461
|
+
await this.assertLibraryAssets(orgId, [input.assetId], "A download");
|
|
377
462
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
378
463
|
const assets = await content.addDownloadAsset(orgId, downloadId, input);
|
|
379
464
|
await outbox.append([contentEvents.downloadAssetAdded.make({
|
|
@@ -390,7 +475,7 @@ var ContentService = class {
|
|
|
390
475
|
downloadId,
|
|
391
476
|
assetId: input.assetId
|
|
392
477
|
});
|
|
393
|
-
return assets;
|
|
478
|
+
return this.downloadFiles(orgId, assets);
|
|
394
479
|
}
|
|
395
480
|
async removeDownloadAsset(orgId, downloadId, assetId) {
|
|
396
481
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -409,7 +494,7 @@ var ContentService = class {
|
|
|
409
494
|
downloadId,
|
|
410
495
|
assetId
|
|
411
496
|
});
|
|
412
|
-
return assets;
|
|
497
|
+
return this.downloadFiles(orgId, assets);
|
|
413
498
|
}
|
|
414
499
|
async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
|
|
415
500
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -428,7 +513,7 @@ var ContentService = class {
|
|
|
428
513
|
downloadId,
|
|
429
514
|
assetId
|
|
430
515
|
});
|
|
431
|
-
return assets;
|
|
516
|
+
return this.downloadFiles(orgId, assets);
|
|
432
517
|
}
|
|
433
518
|
/** The caller must send the COMPLETE ordered set — a partial list would
|
|
434
519
|
* silently unlink whatever it omitted. */
|
|
@@ -460,7 +545,7 @@ var ContentService = class {
|
|
|
460
545
|
orgId,
|
|
461
546
|
downloadId
|
|
462
547
|
});
|
|
463
|
-
return assets;
|
|
548
|
+
return this.downloadFiles(orgId, assets);
|
|
464
549
|
}
|
|
465
550
|
listBundles(orgId, query) {
|
|
466
551
|
return this.repo.listBundles(orgId, query);
|
|
@@ -580,6 +665,416 @@ var ContentService = class {
|
|
|
580
665
|
});
|
|
581
666
|
return items;
|
|
582
667
|
}
|
|
668
|
+
async spaceUpdates(content, orgId, spaceIds, actor) {
|
|
669
|
+
const events = [];
|
|
670
|
+
for (const spaceId of spaceIds) {
|
|
671
|
+
const space = await content.findSpace(orgId, spaceId);
|
|
672
|
+
if (space) events.push(contentEvents.spaceUpdated.make({
|
|
673
|
+
orgId,
|
|
674
|
+
data: space,
|
|
675
|
+
metadata: actorMetadata(actor)
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
return events;
|
|
679
|
+
}
|
|
680
|
+
resolvedSpace(input, current) {
|
|
681
|
+
const accessRule = input.accessRule ?? current?.accessRule ?? "all_members";
|
|
682
|
+
let capacity = input.capacity !== void 0 ? input.capacity : current?.capacity ?? null;
|
|
683
|
+
if (accessRule === "all_members") {
|
|
684
|
+
if (input.capacity != null) throw new InvalidInputError("An all-members space has no capacity");
|
|
685
|
+
capacity = null;
|
|
686
|
+
}
|
|
687
|
+
const settings = {
|
|
688
|
+
...current?.settings ?? spaceSettingsDefaults,
|
|
689
|
+
...input.settings
|
|
690
|
+
};
|
|
691
|
+
return {
|
|
692
|
+
accessRule,
|
|
693
|
+
capacity,
|
|
694
|
+
settings
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
async listCommunities(orgId, query) {
|
|
698
|
+
return this.pageWithThumbnails(await this.repo.listCommunities(orgId, query));
|
|
699
|
+
}
|
|
700
|
+
async getCommunity(orgId, id) {
|
|
701
|
+
const community = await this.repo.findCommunityById(orgId, id);
|
|
702
|
+
return community ? this.withThumbnail(community) : null;
|
|
703
|
+
}
|
|
704
|
+
communitiesWithDigest() {
|
|
705
|
+
return this.repo.communitiesWithDigest();
|
|
706
|
+
}
|
|
707
|
+
listCommunitiesById(orgId, ids) {
|
|
708
|
+
return ids.length ? this.repo.findCommunitiesByIds(orgId, ids) : Promise.resolve([]);
|
|
709
|
+
}
|
|
710
|
+
getSpace(orgId, spaceId) {
|
|
711
|
+
return this.repo.findSpace(orgId, spaceId);
|
|
712
|
+
}
|
|
713
|
+
async getCommunitySpace(orgId, communityId, spaceId) {
|
|
714
|
+
const space = await this.repo.findSpace(orgId, spaceId);
|
|
715
|
+
return space?.communityId === communityId ? space : null;
|
|
716
|
+
}
|
|
717
|
+
async listSpaces(orgId, communityId) {
|
|
718
|
+
if (!await this.getCommunity(orgId, communityId)) throw new NotFoundError("Community", communityId);
|
|
719
|
+
return this.repo.listSpaces(orgId, communityId);
|
|
720
|
+
}
|
|
721
|
+
async getDmSettings(orgId, communityId) {
|
|
722
|
+
return (await this.repo.findCommunityById(orgId, communityId))?.settings.dm ?? null;
|
|
723
|
+
}
|
|
724
|
+
async createCommunity(orgId, input, actor) {
|
|
725
|
+
assertConfigurator(actor);
|
|
726
|
+
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
727
|
+
const created = await content.createCommunity(orgId, input, slugify(input.title));
|
|
728
|
+
await outbox.append([contentEvents.communityCreated.make({
|
|
729
|
+
orgId,
|
|
730
|
+
data: created,
|
|
731
|
+
metadata: actorMetadata(actor)
|
|
732
|
+
})]);
|
|
733
|
+
return created;
|
|
734
|
+
});
|
|
735
|
+
this.logger.info("community created", {
|
|
736
|
+
orgId,
|
|
737
|
+
communityId: community.id,
|
|
738
|
+
actorId: actor.id
|
|
739
|
+
});
|
|
740
|
+
return this.withThumbnail(community);
|
|
741
|
+
}
|
|
742
|
+
async updateCommunity(orgId, id, patch, actor) {
|
|
743
|
+
assertConfigurator(actor);
|
|
744
|
+
await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A community");
|
|
745
|
+
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
746
|
+
const updated = await content.updateCommunity(orgId, id, patch);
|
|
747
|
+
if (!updated) {
|
|
748
|
+
this.logger.warn("community update rejected — not found", {
|
|
749
|
+
orgId,
|
|
750
|
+
communityId: id,
|
|
751
|
+
actorId: actor.id
|
|
752
|
+
});
|
|
753
|
+
throw new NotFoundError("Community", id);
|
|
754
|
+
}
|
|
755
|
+
await outbox.append([contentEvents.communityUpdated.make({
|
|
756
|
+
orgId,
|
|
757
|
+
data: updated,
|
|
758
|
+
metadata: actorMetadata(actor)
|
|
759
|
+
})]);
|
|
760
|
+
return updated;
|
|
761
|
+
});
|
|
762
|
+
this.logger.info("community updated", {
|
|
763
|
+
orgId,
|
|
764
|
+
communityId: id,
|
|
765
|
+
actorId: actor.id
|
|
766
|
+
});
|
|
767
|
+
return this.withThumbnail(community);
|
|
768
|
+
}
|
|
769
|
+
publishCommunity(orgId, id, actor) {
|
|
770
|
+
return this.updateCommunity(orgId, id, { status: "published" }, actor);
|
|
771
|
+
}
|
|
772
|
+
unpublishCommunity(orgId, id, actor) {
|
|
773
|
+
return this.updateCommunity(orgId, id, { status: "draft" }, actor);
|
|
774
|
+
}
|
|
775
|
+
async deleteCommunity(orgId, id, actor) {
|
|
776
|
+
assertConfigurator(actor);
|
|
777
|
+
await this.uow.run(async ({ content, outbox }) => {
|
|
778
|
+
const community = await content.findCommunityById(orgId, id);
|
|
779
|
+
if (!community) {
|
|
780
|
+
this.logger.warn("community delete rejected — not found", {
|
|
781
|
+
orgId,
|
|
782
|
+
communityId: id,
|
|
783
|
+
actorId: actor.id
|
|
784
|
+
});
|
|
785
|
+
throw new NotFoundError("Community", id);
|
|
786
|
+
}
|
|
787
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
788
|
+
if (!await content.deleteCommunity(orgId, id)) throw new NotFoundError("Community", id);
|
|
789
|
+
await content.deleteCommunitySettings(orgId, id);
|
|
790
|
+
await outbox.append([contentEvents.communityDeleted.make({
|
|
791
|
+
orgId,
|
|
792
|
+
data: community,
|
|
793
|
+
metadata: actorMetadata(actor)
|
|
794
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
795
|
+
});
|
|
796
|
+
this.logger.info("community deleted", {
|
|
797
|
+
orgId,
|
|
798
|
+
communityId: id,
|
|
799
|
+
actorId: actor.id
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
async patchCommunitySettings(orgId, id, patch, actor) {
|
|
803
|
+
assertConfigurator(actor);
|
|
804
|
+
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
805
|
+
const updated = await content.patchCommunitySettings(orgId, id, patch);
|
|
806
|
+
if (!updated) {
|
|
807
|
+
this.logger.warn("community settings patch rejected — not found", {
|
|
808
|
+
orgId,
|
|
809
|
+
communityId: id,
|
|
810
|
+
actorId: actor.id
|
|
811
|
+
});
|
|
812
|
+
throw new NotFoundError("Community", id);
|
|
813
|
+
}
|
|
814
|
+
await outbox.append([contentEvents.communityUpdated.make({
|
|
815
|
+
orgId,
|
|
816
|
+
data: updated,
|
|
817
|
+
metadata: actorMetadata(actor)
|
|
818
|
+
})]);
|
|
819
|
+
return updated;
|
|
820
|
+
});
|
|
821
|
+
this.logger.info("community settings patched", {
|
|
822
|
+
orgId,
|
|
823
|
+
communityId: id,
|
|
824
|
+
actorId: actor.id
|
|
825
|
+
});
|
|
826
|
+
return this.withThumbnail(community);
|
|
827
|
+
}
|
|
828
|
+
async createSpace(orgId, communityId, input, actor) {
|
|
829
|
+
assertConfigurator(actor);
|
|
830
|
+
const resolved = this.resolvedSpace(input);
|
|
831
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
832
|
+
if (!await content.findCommunityById(orgId, communityId)) {
|
|
833
|
+
this.logger.warn("space create rejected — community not found", {
|
|
834
|
+
orgId,
|
|
835
|
+
communityId,
|
|
836
|
+
actorId: actor.id
|
|
837
|
+
});
|
|
838
|
+
throw new NotFoundError("Community", communityId);
|
|
839
|
+
}
|
|
840
|
+
const created = await content.createSpace(orgId, communityId, {
|
|
841
|
+
...input,
|
|
842
|
+
accessRule: resolved.accessRule,
|
|
843
|
+
capacity: resolved.capacity,
|
|
844
|
+
settings: resolved.settings
|
|
845
|
+
});
|
|
846
|
+
await outbox.append([contentEvents.spaceCreated.make({
|
|
847
|
+
orgId,
|
|
848
|
+
data: created,
|
|
849
|
+
metadata: actorMetadata(actor)
|
|
850
|
+
})]);
|
|
851
|
+
return created;
|
|
852
|
+
});
|
|
853
|
+
this.logger.info("space created", {
|
|
854
|
+
orgId,
|
|
855
|
+
communityId,
|
|
856
|
+
spaceId: space.id,
|
|
857
|
+
actorId: actor.id
|
|
858
|
+
});
|
|
859
|
+
return space;
|
|
860
|
+
}
|
|
861
|
+
async updateSpace(orgId, communityId, spaceId, patch, actor) {
|
|
862
|
+
assertConfigurator(actor);
|
|
863
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
864
|
+
const current = await content.findSpace(orgId, spaceId);
|
|
865
|
+
if (!current || current.communityId !== communityId) {
|
|
866
|
+
this.logger.warn("space update rejected — not found", {
|
|
867
|
+
orgId,
|
|
868
|
+
communityId,
|
|
869
|
+
spaceId,
|
|
870
|
+
actorId: actor.id
|
|
871
|
+
});
|
|
872
|
+
throw new NotFoundError("Space", spaceId);
|
|
873
|
+
}
|
|
874
|
+
const resolved = this.resolvedSpace(patch, current);
|
|
875
|
+
const updated = await content.updateSpace(orgId, communityId, spaceId, {
|
|
876
|
+
...patch,
|
|
877
|
+
accessRule: resolved.accessRule,
|
|
878
|
+
capacity: resolved.capacity,
|
|
879
|
+
settings: resolved.settings
|
|
880
|
+
});
|
|
881
|
+
if (!updated) throw new NotFoundError("Space", spaceId);
|
|
882
|
+
await outbox.append([contentEvents.spaceUpdated.make({
|
|
883
|
+
orgId,
|
|
884
|
+
data: updated,
|
|
885
|
+
metadata: actorMetadata(actor)
|
|
886
|
+
})]);
|
|
887
|
+
return updated;
|
|
888
|
+
});
|
|
889
|
+
this.logger.info("space updated", {
|
|
890
|
+
orgId,
|
|
891
|
+
communityId,
|
|
892
|
+
spaceId,
|
|
893
|
+
actorId: actor.id
|
|
894
|
+
});
|
|
895
|
+
return space;
|
|
896
|
+
}
|
|
897
|
+
async deleteSpace(orgId, communityId, spaceId, actor) {
|
|
898
|
+
assertConfigurator(actor);
|
|
899
|
+
await this.uow.run(async ({ content, outbox }) => {
|
|
900
|
+
const space = await content.findSpace(orgId, spaceId);
|
|
901
|
+
if (!space || space.communityId !== communityId) {
|
|
902
|
+
this.logger.warn("space delete rejected — not found", {
|
|
903
|
+
orgId,
|
|
904
|
+
communityId,
|
|
905
|
+
spaceId,
|
|
906
|
+
actorId: actor.id
|
|
907
|
+
});
|
|
908
|
+
throw new NotFoundError("Space", spaceId);
|
|
909
|
+
}
|
|
910
|
+
if (!await content.deleteSpace(orgId, communityId, spaceId)) throw new NotFoundError("Space", spaceId);
|
|
911
|
+
await outbox.append([contentEvents.spaceDeleted.make({
|
|
912
|
+
orgId,
|
|
913
|
+
data: space,
|
|
914
|
+
metadata: actorMetadata(actor)
|
|
915
|
+
})]);
|
|
916
|
+
});
|
|
917
|
+
this.logger.info("space deleted", {
|
|
918
|
+
orgId,
|
|
919
|
+
communityId,
|
|
920
|
+
spaceId,
|
|
921
|
+
actorId: actor.id
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
async reorderSpaces(orgId, communityId, orderedIds, actor) {
|
|
925
|
+
assertConfigurator(actor);
|
|
926
|
+
const spaces = await this.uow.run(async ({ content, outbox }) => {
|
|
927
|
+
if (!await content.findCommunityById(orgId, communityId)) {
|
|
928
|
+
this.logger.warn("spaces reorder rejected — community not found", {
|
|
929
|
+
orgId,
|
|
930
|
+
communityId,
|
|
931
|
+
actorId: actor.id
|
|
932
|
+
});
|
|
933
|
+
throw new NotFoundError("Community", communityId);
|
|
934
|
+
}
|
|
935
|
+
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");
|
|
936
|
+
const reordered = await content.reorderSpaces(orgId, communityId, orderedIds);
|
|
937
|
+
await outbox.append([contentEvents.spacesReordered.make({
|
|
938
|
+
orgId,
|
|
939
|
+
data: reordered,
|
|
940
|
+
metadata: actorMetadata(actor)
|
|
941
|
+
})]);
|
|
942
|
+
return reordered;
|
|
943
|
+
});
|
|
944
|
+
this.logger.info("spaces reordered", {
|
|
945
|
+
orgId,
|
|
946
|
+
communityId,
|
|
947
|
+
actorId: actor.id
|
|
948
|
+
});
|
|
949
|
+
return spaces;
|
|
950
|
+
}
|
|
951
|
+
async setSpaceTopics(orgId, communityId, spaceId, input, actor) {
|
|
952
|
+
assertConfigurator(actor);
|
|
953
|
+
const names = input.topics.map((topic) => topic.name);
|
|
954
|
+
if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
|
|
955
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
956
|
+
const locked = await content.lockSpace(orgId, spaceId);
|
|
957
|
+
if (!locked || locked.communityId !== communityId) {
|
|
958
|
+
this.logger.warn("space topics rejected — space not found", {
|
|
959
|
+
orgId,
|
|
960
|
+
communityId,
|
|
961
|
+
spaceId,
|
|
962
|
+
actorId: actor.id
|
|
963
|
+
});
|
|
964
|
+
throw new NotFoundError("Space", spaceId);
|
|
965
|
+
}
|
|
966
|
+
const current = await content.findSpace(orgId, spaceId);
|
|
967
|
+
if (!current) throw new NotFoundError("Space", spaceId);
|
|
968
|
+
const kept = new Set(input.topics.flatMap((topic) => topic.id ? [topic.id] : []));
|
|
969
|
+
const removed = current.topics.filter((topic) => !kept.has(topic.id)).map((topic) => topic.id);
|
|
970
|
+
if (removed.length > 0 && (await content.topicsInUse(orgId, spaceId, removed)).length > 0) throw new ConflictError("A topic used by a post cannot be removed");
|
|
971
|
+
const updated = await content.setSpaceTopics(orgId, spaceId, input);
|
|
972
|
+
await outbox.append([contentEvents.spaceUpdated.make({
|
|
973
|
+
orgId,
|
|
974
|
+
data: updated,
|
|
975
|
+
metadata: actorMetadata(actor)
|
|
976
|
+
})]);
|
|
977
|
+
return updated;
|
|
978
|
+
});
|
|
979
|
+
this.logger.info("space topics set", {
|
|
980
|
+
orgId,
|
|
981
|
+
communityId,
|
|
982
|
+
spaceId,
|
|
983
|
+
actorId: actor.id
|
|
984
|
+
});
|
|
985
|
+
return space;
|
|
986
|
+
}
|
|
987
|
+
async addSpaceLink(orgId, communityId, spaceId, contentId, actor) {
|
|
988
|
+
assertConfigurator(actor);
|
|
989
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
990
|
+
const current = await content.findSpace(orgId, spaceId);
|
|
991
|
+
if (!current || current.communityId !== communityId) {
|
|
992
|
+
this.logger.warn("space link add rejected — space not found", {
|
|
993
|
+
orgId,
|
|
994
|
+
communityId,
|
|
995
|
+
spaceId,
|
|
996
|
+
actorId: actor.id
|
|
997
|
+
});
|
|
998
|
+
throw new NotFoundError("Space", spaceId);
|
|
999
|
+
}
|
|
1000
|
+
if (current.links.some((link) => link.contentId === contentId)) throw new ConflictError("This space already links to that content");
|
|
1001
|
+
const updated = await content.addSpaceLink(orgId, spaceId, contentId);
|
|
1002
|
+
await outbox.append([contentEvents.spaceUpdated.make({
|
|
1003
|
+
orgId,
|
|
1004
|
+
data: updated,
|
|
1005
|
+
metadata: actorMetadata(actor)
|
|
1006
|
+
})]);
|
|
1007
|
+
return updated;
|
|
1008
|
+
});
|
|
1009
|
+
this.logger.info("space link added", {
|
|
1010
|
+
orgId,
|
|
1011
|
+
communityId,
|
|
1012
|
+
spaceId,
|
|
1013
|
+
contentId,
|
|
1014
|
+
actorId: actor.id
|
|
1015
|
+
});
|
|
1016
|
+
return space;
|
|
1017
|
+
}
|
|
1018
|
+
async removeSpaceLink(orgId, communityId, spaceId, linkId, actor) {
|
|
1019
|
+
assertConfigurator(actor);
|
|
1020
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1021
|
+
const current = await content.findSpace(orgId, spaceId);
|
|
1022
|
+
if (!current || current.communityId !== communityId) {
|
|
1023
|
+
this.logger.warn("space link remove rejected — space not found", {
|
|
1024
|
+
orgId,
|
|
1025
|
+
communityId,
|
|
1026
|
+
spaceId,
|
|
1027
|
+
actorId: actor.id
|
|
1028
|
+
});
|
|
1029
|
+
throw new NotFoundError("Space", spaceId);
|
|
1030
|
+
}
|
|
1031
|
+
const updated = await content.removeSpaceLink(orgId, spaceId, linkId);
|
|
1032
|
+
await outbox.append([contentEvents.spaceUpdated.make({
|
|
1033
|
+
orgId,
|
|
1034
|
+
data: updated,
|
|
1035
|
+
metadata: actorMetadata(actor)
|
|
1036
|
+
})]);
|
|
1037
|
+
return updated;
|
|
1038
|
+
});
|
|
1039
|
+
this.logger.info("space link removed", {
|
|
1040
|
+
orgId,
|
|
1041
|
+
communityId,
|
|
1042
|
+
spaceId,
|
|
1043
|
+
linkId,
|
|
1044
|
+
actorId: actor.id
|
|
1045
|
+
});
|
|
1046
|
+
return space;
|
|
1047
|
+
}
|
|
1048
|
+
async reorderSpaceLinks(orgId, communityId, spaceId, orderedIds, actor) {
|
|
1049
|
+
assertConfigurator(actor);
|
|
1050
|
+
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1051
|
+
const current = await content.findSpace(orgId, spaceId);
|
|
1052
|
+
if (!current || current.communityId !== communityId) {
|
|
1053
|
+
this.logger.warn("space links reorder rejected — space not found", {
|
|
1054
|
+
orgId,
|
|
1055
|
+
communityId,
|
|
1056
|
+
spaceId,
|
|
1057
|
+
actorId: actor.id
|
|
1058
|
+
});
|
|
1059
|
+
throw new NotFoundError("Space", spaceId);
|
|
1060
|
+
}
|
|
1061
|
+
if (!sameIdSet(current.links.map((link) => link.id), orderedIds)) throw new ConflictError("Ordered link ids do not match the space's links");
|
|
1062
|
+
const updated = await content.reorderSpaceLinks(orgId, spaceId, orderedIds);
|
|
1063
|
+
await outbox.append([contentEvents.spaceUpdated.make({
|
|
1064
|
+
orgId,
|
|
1065
|
+
data: updated,
|
|
1066
|
+
metadata: actorMetadata(actor)
|
|
1067
|
+
})]);
|
|
1068
|
+
return updated;
|
|
1069
|
+
});
|
|
1070
|
+
this.logger.info("space links reordered", {
|
|
1071
|
+
orgId,
|
|
1072
|
+
communityId,
|
|
1073
|
+
spaceId,
|
|
1074
|
+
actorId: actor.id
|
|
1075
|
+
});
|
|
1076
|
+
return space;
|
|
1077
|
+
}
|
|
583
1078
|
};
|
|
584
1079
|
//#endregion
|
|
585
1080
|
export { ContentService };
|