@headless-lms/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +90 -9
- package/dist/assets/events.js +19 -3
- 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 +5 -4
- package/dist/assets/index.js +3 -2
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +45 -15
- package/dist/assets/service.d.ts +39 -7
- package/dist/assets/service.js +292 -37
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +12 -15
- package/dist/automations/actions.js +188 -30
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +5 -1
- package/dist/automations/catalog.js +514 -197
- 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 +112 -0
- package/dist/automations/community-mail.js +90 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/community-notifications.d.ts +16 -0
- package/dist/automations/community-notifications.js +84 -0
- package/dist/automations/community-notifications.js.map +1 -0
- package/dist/automations/events.d.ts +74 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +7 -5
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +20 -3
- package/dist/automations/model.js +36 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +27 -8
- 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/seeds.d.ts +17 -0
- package/dist/automations/seeds.js +235 -0
- package/dist/automations/seeds.js.map +1 -0
- package/dist/automations/service.d.ts +41 -6
- package/dist/automations/service.js +542 -59
- package/dist/automations/service.js.map +1 -1
- package/dist/automations/types.d.ts +2 -2
- package/dist/automations/webhook-signing.d.ts +7 -0
- package/dist/automations/webhook-signing.js +22 -0
- package/dist/automations/webhook-signing.js.map +1 -0
- package/dist/content/events.d.ts +1013 -25
- package/dist/content/events.js +168 -19
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +5 -5
- package/dist/content/index.js +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +109 -25
- package/dist/content/service.d.ts +68 -23
- package/dist/content/service.js +647 -60
- package/dist/content/service.js.map +1 -1
- 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 +104 -0
- package/dist/discussion/chats.js +798 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/events.d.ts +633 -14
- package/dist/discussion/events.js +134 -5
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -3
- package/dist/discussion/index.js +4 -1
- package/dist/discussion/model.d.ts +1 -2
- package/dist/discussion/ports.d.ts +288 -27
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +103 -0
- package/dist/discussion/posts.js +723 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +33 -11
- package/dist/discussion/service.js +297 -81
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +264 -13
- package/dist/entitlements/events.js +49 -5
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +124 -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 +54 -3
- package/dist/entitlements/service.js +473 -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/events.d.ts +17 -3
- package/dist/identity/events.js +12 -2
- package/dist/identity/events.js.map +1 -1
- package/dist/identity/index.d.ts +6 -4
- package/dist/identity/index.js +3 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notifications.d.ts +27 -0
- package/dist/identity/notifications.js +101 -0
- package/dist/identity/notifications.js.map +1 -0
- package/dist/identity/ports.d.ts +76 -3
- package/dist/identity/service.d.ts +17 -3
- package/dist/identity/service.js +73 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +27 -3
- package/dist/integrations/events.js +15 -3
- package/dist/integrations/events.js.map +1 -1
- 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 +234 -25
- package/dist/organizations/events.js +58 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -7
- package/dist/organizations/index.js +4 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +20 -21
- 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 +28 -14
- package/dist/organizations/service.js +274 -105
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -11
- package/dist/progress/events.d.ts +34 -2
- package/dist/progress/events.js +15 -2
- package/dist/progress/events.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +5 -3
- package/dist/progress/service.js +9 -3
- 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 +124 -0
- package/dist/reporting/member/ports.d.ts +45 -0
- package/dist/reporting/member/service.d.ts +73 -0
- package/dist/reporting/member/service.js +286 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +7 -1
- package/dist/shared/errors.js +10 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/events.d.ts +5 -3
- package/dist/shared/events.js +1 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/id.d.ts +12 -0
- package/dist/shared/id.js +13 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +6 -1
- 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/settings.js.map +1 -1
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +21 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +12 -3
- package/dist/types/email-templates.d.ts +124 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/events.d.ts +3 -3
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +10 -10
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +7 -1
- package/dist/types/schemas/assets.d.ts +67 -36
- package/dist/types/schemas/assets.js +19 -13
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +88 -4
- package/dist/types/schemas/automations.js +46 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +385 -20
- package/dist/types/schemas/content.js +178 -15
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/credentials.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +379 -4
- package/dist/types/schemas/discussion.js +209 -7
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +44 -4
- package/dist/types/schemas/entitlements.js +32 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +115 -5
- package/dist/types/schemas/identity.js +59 -6
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -9
- package/dist/types/schemas/index.js +9 -9
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +27 -5
- package/dist/types/schemas/organizations.js +14 -7
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/outbox.d.ts +1 -0
- package/dist/types/schemas/outbox.js +1 -0
- package/dist/types/schemas/outbox.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +1 -0
- package/dist/types/schemas/progress.js +1 -0
- package/dist/types/schemas/progress.js.map +1 -1
- package/dist/types/schemas/settings.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +4 -1
- package/dist/types/schemas/shared.js +7 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +2 -2
- 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 -42
- package/dist/reporting/learn/service.d.ts +0 -48
- package/dist/reporting/learn/service.js +0 -111
- 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 -31
- package/dist/reporting/students/ports.d.ts +0 -13
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -19
- package/dist/reporting/students/service.js +0 -20
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{dashboard → admin}/model.js +0 -0
- /package/dist/reporting/{dashboard → admin}/ports.js +0 -0
- /package/dist/reporting/{learn → member}/model.js +0 -0
- /package/dist/reporting/{learn → member}/ports.js +0 -0
- /package/dist/{reporting/students/model.js → shared/actor.js} +0 -0
package/dist/content/service.js
CHANGED
|
@@ -1,49 +1,71 @@
|
|
|
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 "../shared/settings.js";
|
|
4
|
-
import { ConflictError, NotFoundError } from "../shared/errors.js";
|
|
5
5
|
//#region src/content/service.ts
|
|
6
6
|
/** URL-safe slug derived from a title. Domain rule owned by the service. */
|
|
7
7
|
function slugify(title) {
|
|
8
8
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function toCourseSettings(stored) {
|
|
13
|
-
return {
|
|
14
|
-
...settingsDefaults,
|
|
15
|
-
...stored
|
|
16
|
-
};
|
|
10
|
+
function actorMetadata(actor) {
|
|
11
|
+
return { actorOrgUserId: actor.id };
|
|
17
12
|
}
|
|
18
|
-
|
|
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
|
+
}
|
|
21
|
+
var ContentService = class {
|
|
19
22
|
repo;
|
|
20
23
|
uow;
|
|
21
|
-
|
|
24
|
+
libraryAssets;
|
|
25
|
+
files;
|
|
26
|
+
fileUrls;
|
|
22
27
|
logger;
|
|
23
28
|
constructor(params) {
|
|
24
29
|
this.repo = params.repo;
|
|
25
30
|
this.uow = params.uow;
|
|
26
|
-
this.
|
|
31
|
+
this.libraryAssets = params.libraryAssets;
|
|
32
|
+
this.files = params.files;
|
|
33
|
+
this.fileUrls = params.fileUrls;
|
|
27
34
|
this.logger = params.logger ?? noopLogger;
|
|
28
35
|
}
|
|
29
|
-
|
|
30
|
-
return
|
|
36
|
+
withThumbnail(item) {
|
|
37
|
+
return {
|
|
38
|
+
...item,
|
|
39
|
+
thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.thumbnail(item.type, item.id) : null
|
|
40
|
+
};
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
const course = await this.repo.findById(orgId, id);
|
|
34
|
-
if (!course) return null;
|
|
35
|
-
const settings = await this.settings.get(orgId, "content", id);
|
|
42
|
+
pageWithThumbnails(page) {
|
|
36
43
|
return {
|
|
37
|
-
...
|
|
38
|
-
|
|
39
|
-
...toCourseSettings(course.settings),
|
|
40
|
-
...settings
|
|
41
|
-
}
|
|
44
|
+
...page,
|
|
45
|
+
rows: page.rows.map((row) => this.withThumbnail(row))
|
|
42
46
|
};
|
|
43
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;
|
|
65
|
+
}
|
|
44
66
|
async createCourse(orgId, input) {
|
|
45
67
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
46
|
-
const created = await content.
|
|
68
|
+
const created = await content.createCourse(orgId, input, slugify(input.title));
|
|
47
69
|
await outbox.append([contentEvents.courseCreated.make({
|
|
48
70
|
orgId,
|
|
49
71
|
data: created
|
|
@@ -55,11 +77,12 @@ var ContentServiceImpl = class {
|
|
|
55
77
|
courseId: course.id,
|
|
56
78
|
title: course.title
|
|
57
79
|
});
|
|
58
|
-
return course;
|
|
80
|
+
return this.withThumbnail(course);
|
|
59
81
|
}
|
|
60
82
|
async updateCourse(orgId, id, patch) {
|
|
83
|
+
await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A course");
|
|
61
84
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
62
|
-
const updated = await content.
|
|
85
|
+
const updated = await content.updateCourse(orgId, id, patch);
|
|
63
86
|
if (!updated) {
|
|
64
87
|
this.logger.warn("course update rejected — not found", {
|
|
65
88
|
orgId,
|
|
@@ -78,31 +101,38 @@ var ContentServiceImpl = class {
|
|
|
78
101
|
courseId: id,
|
|
79
102
|
fields: Object.keys(patch)
|
|
80
103
|
});
|
|
81
|
-
return course;
|
|
104
|
+
return this.withThumbnail(course);
|
|
82
105
|
}
|
|
83
|
-
async
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
async patchCourseSettings(orgId, id, value) {
|
|
107
|
+
const merged = await this.uow.run(async ({ content, outbox }) => {
|
|
108
|
+
const course = await content.findCourseById(orgId, id);
|
|
109
|
+
if (!course) {
|
|
110
|
+
this.logger.warn("course settings patch rejected — not found", {
|
|
111
|
+
orgId,
|
|
112
|
+
courseId: id
|
|
113
|
+
});
|
|
114
|
+
throw new NotFoundError("Course", id);
|
|
115
|
+
}
|
|
116
|
+
const settings = await content.patchCourseSettings(orgId, id, value);
|
|
117
|
+
await outbox.append([contentEvents.courseUpdated.make({
|
|
87
118
|
orgId,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
data: {
|
|
120
|
+
...course,
|
|
121
|
+
settings
|
|
122
|
+
}
|
|
123
|
+
})]);
|
|
124
|
+
return settings;
|
|
125
|
+
});
|
|
93
126
|
this.logger.info("course settings patched", {
|
|
94
127
|
orgId,
|
|
95
128
|
courseId: id,
|
|
96
129
|
value
|
|
97
130
|
});
|
|
98
|
-
return
|
|
99
|
-
...toCourseSettings(course.settings),
|
|
100
|
-
...merged
|
|
101
|
-
};
|
|
131
|
+
return merged;
|
|
102
132
|
}
|
|
103
|
-
async deleteCourse(orgId, id) {
|
|
133
|
+
async deleteCourse(orgId, id, actor) {
|
|
104
134
|
await this.uow.run(async ({ content, outbox }) => {
|
|
105
|
-
const course = await content.
|
|
135
|
+
const course = await content.findCourseById(orgId, id);
|
|
106
136
|
if (!course) {
|
|
107
137
|
this.logger.warn("course delete rejected — not found", {
|
|
108
138
|
orgId,
|
|
@@ -110,11 +140,12 @@ var ContentServiceImpl = class {
|
|
|
110
140
|
});
|
|
111
141
|
throw new NotFoundError("Course", id);
|
|
112
142
|
}
|
|
113
|
-
|
|
143
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
144
|
+
if (!await content.deleteCourse(orgId, id)) throw new NotFoundError("Course", id);
|
|
114
145
|
await outbox.append([contentEvents.courseDeleted.make({
|
|
115
146
|
orgId,
|
|
116
147
|
data: course
|
|
117
|
-
})]);
|
|
148
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
118
149
|
});
|
|
119
150
|
this.logger.info("course deleted", {
|
|
120
151
|
orgId,
|
|
@@ -122,11 +153,21 @@ var ContentServiceImpl = class {
|
|
|
122
153
|
});
|
|
123
154
|
}
|
|
124
155
|
listCourseModules(orgId, courseId) {
|
|
125
|
-
return this.repo.
|
|
156
|
+
return this.repo.listCourseModules(orgId, courseId);
|
|
126
157
|
}
|
|
127
158
|
listCourseActivities(orgId, courseId) {
|
|
128
159
|
return this.repo.listActivitiesForCourse(orgId, courseId);
|
|
129
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
|
+
}
|
|
168
|
+
listCourseActivityAssets(orgId, courseId) {
|
|
169
|
+
return this.repo.listActivityAssetsForCourse(orgId, courseId);
|
|
170
|
+
}
|
|
130
171
|
getActivity(orgId, activityId) {
|
|
131
172
|
return this.repo.findActivity(orgId, activityId);
|
|
132
173
|
}
|
|
@@ -242,7 +283,21 @@ var ContentServiceImpl = class {
|
|
|
242
283
|
});
|
|
243
284
|
return modules;
|
|
244
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
|
+
}
|
|
245
299
|
async saveActivity(orgId, courseId, moduleId, input, activityId) {
|
|
300
|
+
await this.assertLibraryAssets(orgId, input.assetIds ?? []);
|
|
246
301
|
const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
|
|
247
302
|
if (activityId) {
|
|
248
303
|
const existing = await content.findActivity(orgId, activityId);
|
|
@@ -314,11 +369,12 @@ var ContentServiceImpl = class {
|
|
|
314
369
|
});
|
|
315
370
|
return modules;
|
|
316
371
|
}
|
|
317
|
-
listDownloads(orgId, query) {
|
|
318
|
-
return this.repo.listDownloads(orgId, query);
|
|
372
|
+
async listDownloads(orgId, query) {
|
|
373
|
+
return this.pageWithThumbnails(await this.repo.listDownloads(orgId, query));
|
|
319
374
|
}
|
|
320
|
-
getDownload(orgId, id) {
|
|
321
|
-
|
|
375
|
+
async getDownload(orgId, id) {
|
|
376
|
+
const download = await this.repo.getDownload(orgId, id);
|
|
377
|
+
return download ? this.withThumbnail(download) : null;
|
|
322
378
|
}
|
|
323
379
|
async createDownload(orgId, input) {
|
|
324
380
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -334,9 +390,10 @@ var ContentServiceImpl = class {
|
|
|
334
390
|
downloadId: download.id,
|
|
335
391
|
title: download.title
|
|
336
392
|
});
|
|
337
|
-
return download;
|
|
393
|
+
return this.withThumbnail(download);
|
|
338
394
|
}
|
|
339
395
|
async updateDownload(orgId, id, patch) {
|
|
396
|
+
await this.assertLibraryAssets(orgId, [patch.thumbnailAssetId], "A download");
|
|
340
397
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
341
398
|
const updated = await content.updateDownload(orgId, id, patch);
|
|
342
399
|
if (!updated) {
|
|
@@ -357,9 +414,9 @@ var ContentServiceImpl = class {
|
|
|
357
414
|
downloadId: id,
|
|
358
415
|
fields: Object.keys(patch)
|
|
359
416
|
});
|
|
360
|
-
return download;
|
|
417
|
+
return this.withThumbnail(download);
|
|
361
418
|
}
|
|
362
|
-
async removeDownload(orgId, id) {
|
|
419
|
+
async removeDownload(orgId, id, actor) {
|
|
363
420
|
await this.uow.run(async ({ content, outbox }) => {
|
|
364
421
|
const download = await content.getDownload(orgId, id);
|
|
365
422
|
if (!download) {
|
|
@@ -369,21 +426,23 @@ var ContentServiceImpl = class {
|
|
|
369
426
|
});
|
|
370
427
|
throw new NotFoundError("Download", id);
|
|
371
428
|
}
|
|
429
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
372
430
|
if (!await content.deleteDownload(orgId, id)) throw new NotFoundError("Download", id);
|
|
373
431
|
await outbox.append([contentEvents.downloadDeleted.make({
|
|
374
432
|
orgId,
|
|
375
433
|
data: download
|
|
376
|
-
})]);
|
|
434
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
377
435
|
});
|
|
378
436
|
this.logger.info("download deleted", {
|
|
379
437
|
orgId,
|
|
380
438
|
downloadId: id
|
|
381
439
|
});
|
|
382
440
|
}
|
|
383
|
-
listDownloadAssets(orgId, downloadId) {
|
|
384
|
-
return this.repo.listDownloadAssets(orgId, downloadId);
|
|
441
|
+
async listDownloadAssets(orgId, downloadId) {
|
|
442
|
+
return this.downloadFiles(orgId, await this.repo.listDownloadAssets(orgId, downloadId));
|
|
385
443
|
}
|
|
386
444
|
async addDownloadAsset(orgId, downloadId, input) {
|
|
445
|
+
await this.assertLibraryAssets(orgId, [input.assetId], "A download");
|
|
387
446
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
388
447
|
const assets = await content.addDownloadAsset(orgId, downloadId, input);
|
|
389
448
|
await outbox.append([contentEvents.downloadAssetAdded.make({
|
|
@@ -400,7 +459,7 @@ var ContentServiceImpl = class {
|
|
|
400
459
|
downloadId,
|
|
401
460
|
assetId: input.assetId
|
|
402
461
|
});
|
|
403
|
-
return assets;
|
|
462
|
+
return this.downloadFiles(orgId, assets);
|
|
404
463
|
}
|
|
405
464
|
async removeDownloadAsset(orgId, downloadId, assetId) {
|
|
406
465
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -419,7 +478,7 @@ var ContentServiceImpl = class {
|
|
|
419
478
|
downloadId,
|
|
420
479
|
assetId
|
|
421
480
|
});
|
|
422
|
-
return assets;
|
|
481
|
+
return this.downloadFiles(orgId, assets);
|
|
423
482
|
}
|
|
424
483
|
async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
|
|
425
484
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -438,7 +497,7 @@ var ContentServiceImpl = class {
|
|
|
438
497
|
downloadId,
|
|
439
498
|
assetId
|
|
440
499
|
});
|
|
441
|
-
return assets;
|
|
500
|
+
return this.downloadFiles(orgId, assets);
|
|
442
501
|
}
|
|
443
502
|
/** The caller must send the COMPLETE ordered set — a partial list would
|
|
444
503
|
* silently unlink whatever it omitted. */
|
|
@@ -470,10 +529,538 @@ var ContentServiceImpl = class {
|
|
|
470
529
|
orgId,
|
|
471
530
|
downloadId
|
|
472
531
|
});
|
|
473
|
-
return assets;
|
|
532
|
+
return this.downloadFiles(orgId, assets);
|
|
533
|
+
}
|
|
534
|
+
listBundles(orgId, query) {
|
|
535
|
+
return this.repo.listBundles(orgId, query);
|
|
536
|
+
}
|
|
537
|
+
getBundle(orgId, id) {
|
|
538
|
+
return this.repo.findBundleById(orgId, id);
|
|
539
|
+
}
|
|
540
|
+
async createBundle(orgId, input) {
|
|
541
|
+
const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
|
|
542
|
+
const created = await content.createBundle(orgId, input);
|
|
543
|
+
const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
|
|
544
|
+
await outbox.append([contentEvents.bundleCreated.make({
|
|
545
|
+
orgId,
|
|
546
|
+
data: created
|
|
547
|
+
}), ...initial.length > 0 ? [contentEvents.bundleItemAdded.make({
|
|
548
|
+
orgId,
|
|
549
|
+
data: {
|
|
550
|
+
bundleId: created.id,
|
|
551
|
+
items: initial
|
|
552
|
+
}
|
|
553
|
+
})] : []]);
|
|
554
|
+
return {
|
|
555
|
+
bundle: created,
|
|
556
|
+
items: initial
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
this.logger.info("bundle created", {
|
|
560
|
+
orgId,
|
|
561
|
+
bundleId: bundle.id,
|
|
562
|
+
name: bundle.name,
|
|
563
|
+
itemCount: items.length
|
|
564
|
+
});
|
|
565
|
+
return bundle;
|
|
566
|
+
}
|
|
567
|
+
async updateBundle(orgId, id, patch) {
|
|
568
|
+
const bundle = await this.uow.run(async ({ content, outbox }) => {
|
|
569
|
+
const updated = await content.updateBundle(orgId, id, patch);
|
|
570
|
+
if (!updated) {
|
|
571
|
+
this.logger.warn("bundle update rejected — not found", {
|
|
572
|
+
orgId,
|
|
573
|
+
bundleId: id
|
|
574
|
+
});
|
|
575
|
+
throw new NotFoundError("Bundle", id);
|
|
576
|
+
}
|
|
577
|
+
await outbox.append([contentEvents.bundleUpdated.make({
|
|
578
|
+
orgId,
|
|
579
|
+
data: updated
|
|
580
|
+
})]);
|
|
581
|
+
return updated;
|
|
582
|
+
});
|
|
583
|
+
this.logger.info("bundle updated", {
|
|
584
|
+
orgId,
|
|
585
|
+
bundleId: id,
|
|
586
|
+
fields: Object.keys(patch)
|
|
587
|
+
});
|
|
588
|
+
return bundle;
|
|
589
|
+
}
|
|
590
|
+
async deleteBundle(orgId, id) {
|
|
591
|
+
await this.uow.run(async ({ content, outbox }) => {
|
|
592
|
+
const bundle = await content.findBundleById(orgId, id);
|
|
593
|
+
if (!bundle) {
|
|
594
|
+
this.logger.warn("bundle delete rejected — not found", {
|
|
595
|
+
orgId,
|
|
596
|
+
bundleId: id
|
|
597
|
+
});
|
|
598
|
+
throw new NotFoundError("Bundle", id);
|
|
599
|
+
}
|
|
600
|
+
if (!await content.deleteBundle(orgId, id)) throw new NotFoundError("Bundle", id);
|
|
601
|
+
await outbox.append([contentEvents.bundleDeleted.make({
|
|
602
|
+
orgId,
|
|
603
|
+
data: bundle
|
|
604
|
+
})]);
|
|
605
|
+
});
|
|
606
|
+
this.logger.info("bundle deleted", {
|
|
607
|
+
orgId,
|
|
608
|
+
bundleId: id
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
listBundleItems(orgId, bundleId) {
|
|
612
|
+
return this.repo.listBundleItems(orgId, bundleId);
|
|
613
|
+
}
|
|
614
|
+
async addBundleItem(orgId, bundleId, contentId) {
|
|
615
|
+
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
616
|
+
const all = await content.addBundleItem(orgId, bundleId, contentId);
|
|
617
|
+
await outbox.append([contentEvents.bundleItemAdded.make({
|
|
618
|
+
orgId,
|
|
619
|
+
data: {
|
|
620
|
+
bundleId,
|
|
621
|
+
items: all
|
|
622
|
+
}
|
|
623
|
+
})]);
|
|
624
|
+
return all;
|
|
625
|
+
});
|
|
626
|
+
this.logger.info("bundle item added", {
|
|
627
|
+
orgId,
|
|
628
|
+
bundleId,
|
|
629
|
+
contentId
|
|
630
|
+
});
|
|
631
|
+
return items;
|
|
632
|
+
}
|
|
633
|
+
async removeBundleItem(orgId, bundleId, contentId) {
|
|
634
|
+
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
635
|
+
const all = await content.removeBundleItem(orgId, bundleId, contentId);
|
|
636
|
+
await outbox.append([contentEvents.bundleItemRemoved.make({
|
|
637
|
+
orgId,
|
|
638
|
+
data: {
|
|
639
|
+
bundleId,
|
|
640
|
+
items: all
|
|
641
|
+
}
|
|
642
|
+
})]);
|
|
643
|
+
return all;
|
|
644
|
+
});
|
|
645
|
+
this.logger.info("bundle item removed", {
|
|
646
|
+
orgId,
|
|
647
|
+
bundleId,
|
|
648
|
+
contentId
|
|
649
|
+
});
|
|
650
|
+
return items;
|
|
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;
|
|
474
1061
|
}
|
|
475
1062
|
};
|
|
476
1063
|
//#endregion
|
|
477
|
-
export {
|
|
1064
|
+
export { ContentService };
|
|
478
1065
|
|
|
479
1066
|
//# sourceMappingURL=service.js.map
|