@headless-lms/core 0.4.0 → 0.6.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 +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +1 -1
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +12 -12
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +90 -61
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +465 -88
- package/dist/content/events.js +34 -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/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +103 -67
- package/dist/content/service.js +739 -152
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +266 -152
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +58 -58
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +2 -1
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +16 -12
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +31 -14
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +21 -6
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
package/dist/content/service.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spaceSettingsDefaults } from "../types/schemas/content.js";
|
|
|
2
2
|
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
3
3
|
import { noopLogger } from "../shared/logger.js";
|
|
4
4
|
import { contentEvents } from "./events.js";
|
|
5
|
+
import { EVENT_LEADS, EVENT_STARTING_SOON_JOB } from "./jobs.js";
|
|
6
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
7
|
//#region src/content/service.ts
|
|
6
8
|
/** URL-safe slug derived from a title. Domain rule owned by the service. */
|
|
7
9
|
function slugify(title) {
|
|
@@ -11,10 +13,35 @@ function isPublishedActivity(settings) {
|
|
|
11
13
|
return settings?.published !== false;
|
|
12
14
|
}
|
|
13
15
|
function actorMetadata(actor) {
|
|
14
|
-
return {
|
|
16
|
+
return { actorId: actor.id };
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const COMMUNITY_READERS = ["communities:read", "communities:moderate"];
|
|
19
|
+
function eventChange(actor, previous, next) {
|
|
20
|
+
return {
|
|
21
|
+
actorId: actor.id,
|
|
22
|
+
previousStatus: previous.status,
|
|
23
|
+
previousType: previous.settings.type,
|
|
24
|
+
previousStartsAt: previous.startsAt.toISOString(),
|
|
25
|
+
previousEndsAt: previous.endsAt.toISOString(),
|
|
26
|
+
locationChanged: !isDeepStrictEqual(previous.location, next.location)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function assertEventTimes(startsAt, endsAt) {
|
|
30
|
+
if (endsAt.getTime() <= startsAt.getTime()) throw new InvalidInputError("An event must end after it starts");
|
|
31
|
+
}
|
|
32
|
+
function startingSoonKey(eventId, lead) {
|
|
33
|
+
return `${eventId}:${lead}`;
|
|
34
|
+
}
|
|
35
|
+
const EVENT_LEAD_KEYS = Object.keys(EVENT_LEADS);
|
|
36
|
+
function shownPublicly(item) {
|
|
37
|
+
return item.status === "published" || item.type === "event" && item.status === "canceled";
|
|
38
|
+
}
|
|
39
|
+
function spaceImageIds(space) {
|
|
40
|
+
return [
|
|
41
|
+
space.coverAssetId,
|
|
42
|
+
space.backgroundAssetId,
|
|
43
|
+
space.iconAssetId
|
|
44
|
+
].filter((id) => id !== null);
|
|
18
45
|
}
|
|
19
46
|
function sameIdSet(current, requested) {
|
|
20
47
|
if (current.length !== requested.length) return false;
|
|
@@ -27,6 +54,7 @@ var ContentService = class {
|
|
|
27
54
|
libraryAssets;
|
|
28
55
|
files;
|
|
29
56
|
fileUrls;
|
|
57
|
+
scheduler;
|
|
30
58
|
logger;
|
|
31
59
|
constructor(params) {
|
|
32
60
|
this.repo = params.repo;
|
|
@@ -34,14 +62,67 @@ var ContentService = class {
|
|
|
34
62
|
this.libraryAssets = params.libraryAssets;
|
|
35
63
|
this.files = params.files;
|
|
36
64
|
this.fileUrls = params.fileUrls;
|
|
65
|
+
this.scheduler = params.scheduler;
|
|
37
66
|
this.logger = params.logger ?? noopLogger;
|
|
38
67
|
}
|
|
68
|
+
require(actor, permission, on) {
|
|
69
|
+
this.requireAny(actor, [permission], on);
|
|
70
|
+
}
|
|
71
|
+
requireAny(actor, permissions, on) {
|
|
72
|
+
if (!permissions.some((permission) => actor.permissions.has(permission, on))) {
|
|
73
|
+
this.logger.warn("content action refused", {
|
|
74
|
+
orgId: actor.orgId,
|
|
75
|
+
actorId: actor.id,
|
|
76
|
+
permission: permissions.join(" or "),
|
|
77
|
+
contentId: on
|
|
78
|
+
});
|
|
79
|
+
throw new ForbiddenError(`You need ${permissions.join(" or ")}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
visibleIds(actor, permissions, what) {
|
|
83
|
+
const wheres = permissions.map((permission) => actor.permissions.where(permission));
|
|
84
|
+
if (wheres.some((where) => where.org)) return;
|
|
85
|
+
const ids = [...new Set(wheres.flatMap((where) => where.content))];
|
|
86
|
+
if (ids.length === 0) {
|
|
87
|
+
this.logger.warn("content list refused", {
|
|
88
|
+
orgId: actor.orgId,
|
|
89
|
+
actorId: actor.id,
|
|
90
|
+
permission: permissions.join(" or ")
|
|
91
|
+
});
|
|
92
|
+
throw new ForbiddenError(`You cannot see ${what}`);
|
|
93
|
+
}
|
|
94
|
+
return ids;
|
|
95
|
+
}
|
|
39
96
|
withThumbnail(item) {
|
|
40
97
|
return {
|
|
41
98
|
...item,
|
|
42
|
-
thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.
|
|
99
|
+
thumbnailUrl: !item.thumbnailAssetId ? null : shownPublicly(item) ? this.fileUrls.publicImage(item.orgId, item.id, item.thumbnailAssetId) : this.fileUrls.thumbnail(item.type, item.id)
|
|
43
100
|
};
|
|
44
101
|
}
|
|
102
|
+
syncThumbnail(item) {
|
|
103
|
+
return this.files.syncPublicImages(item.orgId, item.id, shownPublicly(item) && item.thumbnailAssetId ? [item.thumbnailAssetId] : []);
|
|
104
|
+
}
|
|
105
|
+
withdrawThumbnail(orgId, id) {
|
|
106
|
+
return this.files.syncPublicImages(orgId, id, []);
|
|
107
|
+
}
|
|
108
|
+
spaceView(space, published) {
|
|
109
|
+
const url = (image, assetId) => !assetId ? null : published ? this.fileUrls.publicImage(space.orgId, space.id, assetId) : this.fileUrls.spaceImage(space.id, image);
|
|
110
|
+
return {
|
|
111
|
+
...space,
|
|
112
|
+
coverUrl: url("cover", space.coverAssetId),
|
|
113
|
+
backgroundUrl: url("background", space.backgroundAssetId),
|
|
114
|
+
iconUrl: url("icon", space.iconAssetId)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async communityPublished(orgId, communityId) {
|
|
118
|
+
return (await this.repo.findCommunityById(orgId, communityId))?.status === "published";
|
|
119
|
+
}
|
|
120
|
+
async oneSpaceView(space) {
|
|
121
|
+
return this.spaceView(space, await this.communityPublished(space.orgId, space.communityId));
|
|
122
|
+
}
|
|
123
|
+
syncSpaceImages(space, published) {
|
|
124
|
+
return this.files.syncPublicImages(space.orgId, space.id, published ? spaceImageIds(space) : []);
|
|
125
|
+
}
|
|
45
126
|
pageWithThumbnails(page) {
|
|
46
127
|
return {
|
|
47
128
|
...page,
|
|
@@ -59,14 +140,76 @@ var ContentService = class {
|
|
|
59
140
|
}] : [];
|
|
60
141
|
});
|
|
61
142
|
}
|
|
62
|
-
async
|
|
63
|
-
|
|
143
|
+
async eventFiles(orgId, links) {
|
|
144
|
+
const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
|
|
145
|
+
return links.flatMap((link) => {
|
|
146
|
+
const file = ready[link.assetId];
|
|
147
|
+
return file ? [{
|
|
148
|
+
...link,
|
|
149
|
+
...file,
|
|
150
|
+
url: this.fileUrls.eventFile(link.eventId, link.assetId)
|
|
151
|
+
}] : [];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async assertAttachable(actor, itemId, requested, attached, user) {
|
|
155
|
+
const ids = [...new Set(requested.filter((id) => typeof id === "string"))];
|
|
156
|
+
if (ids.length === 0) return;
|
|
157
|
+
const held = new Set(await attached());
|
|
158
|
+
const added = ids.filter((id) => !held.has(id));
|
|
159
|
+
if (added.length === 0) return;
|
|
160
|
+
const orgId = actor.orgId;
|
|
161
|
+
const found = new Map((await this.libraryAssets.libraryAssetsFor(orgId, added)).map((asset) => [asset.id, asset]));
|
|
162
|
+
const missing = added.filter((id) => !found.has(id));
|
|
163
|
+
if (missing.length > 0) {
|
|
164
|
+
this.logger.warn("library assets refused", {
|
|
165
|
+
orgId,
|
|
166
|
+
contentId: itemId,
|
|
167
|
+
assetIds: missing
|
|
168
|
+
});
|
|
169
|
+
throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${missing.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
if (actor.permissions.has("media:read")) return;
|
|
172
|
+
const foreign = added.filter((id) => found.get(id)?.contentId !== itemId);
|
|
173
|
+
if (foreign.length > 0) {
|
|
174
|
+
this.logger.warn("library assets refused — not uploaded for this item", {
|
|
175
|
+
orgId,
|
|
176
|
+
actorId: actor.id,
|
|
177
|
+
contentId: itemId,
|
|
178
|
+
assetIds: foreign
|
|
179
|
+
});
|
|
180
|
+
throw new ForbiddenError(`You need media:read to use files that were not uploaded for this item: ${foreign.join(", ")}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async courseAssetIds(orgId, courseId) {
|
|
184
|
+
const [course, links] = await Promise.all([this.repo.findCourseById(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
|
|
185
|
+
return [course?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
|
|
186
|
+
}
|
|
187
|
+
async downloadAssetIds(orgId, downloadId) {
|
|
188
|
+
const [download, links] = await Promise.all([this.repo.getDownload(orgId, downloadId), this.repo.listDownloadAssets(orgId, downloadId)]);
|
|
189
|
+
return [download?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
|
|
190
|
+
}
|
|
191
|
+
async eventAssetIds(orgId, eventId) {
|
|
192
|
+
const [event, links] = await Promise.all([this.repo.findEventById(orgId, eventId), this.repo.listEventAssets(orgId, eventId)]);
|
|
193
|
+
if (!event) throw new NotFoundError("Event", eventId);
|
|
194
|
+
return [event.thumbnailAssetId, ...links.map((link) => link.assetId)];
|
|
195
|
+
}
|
|
196
|
+
async listCourses(query, actor) {
|
|
197
|
+
const ids = this.visibleIds(actor, ["courses:read"], "courses");
|
|
198
|
+
return this.pageWithThumbnails(await this.repo.listCourses(actor.orgId, query, ids));
|
|
199
|
+
}
|
|
200
|
+
async getCourse(id, actor) {
|
|
201
|
+
const course = await this.findCourse(id, actor);
|
|
202
|
+
if (!course) throw new NotFoundError("Course", id);
|
|
203
|
+
return course;
|
|
64
204
|
}
|
|
65
|
-
async
|
|
66
|
-
|
|
205
|
+
async findCourse(id, actor) {
|
|
206
|
+
this.require(actor, "courses:read", id);
|
|
207
|
+
const course = await this.repo.findCourseById(actor.orgId, id);
|
|
67
208
|
return course ? this.withThumbnail(course) : null;
|
|
68
209
|
}
|
|
69
|
-
async createCourse(
|
|
210
|
+
async createCourse(input, actor) {
|
|
211
|
+
this.require(actor, "courses:write");
|
|
212
|
+
const orgId = actor.orgId;
|
|
70
213
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
71
214
|
const created = await content.createCourse(orgId, input, slugify(input.title));
|
|
72
215
|
await outbox.append([contentEvents.courseCreated.make({
|
|
@@ -78,12 +221,15 @@ var ContentService = class {
|
|
|
78
221
|
this.logger.info("course created", {
|
|
79
222
|
orgId,
|
|
80
223
|
courseId: course.id,
|
|
81
|
-
title: course.title
|
|
224
|
+
title: course.title,
|
|
225
|
+
actorId: actor.id
|
|
82
226
|
});
|
|
83
227
|
return this.withThumbnail(course);
|
|
84
228
|
}
|
|
85
|
-
async updateCourse(
|
|
86
|
-
|
|
229
|
+
async updateCourse(id, patch, actor) {
|
|
230
|
+
this.require(actor, "courses:write", id);
|
|
231
|
+
const orgId = actor.orgId;
|
|
232
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.courseAssetIds(orgId, id), "A course");
|
|
87
233
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
88
234
|
const updated = await content.updateCourse(orgId, id, patch);
|
|
89
235
|
if (!updated) {
|
|
@@ -102,11 +248,15 @@ var ContentService = class {
|
|
|
102
248
|
this.logger.info("course updated", {
|
|
103
249
|
orgId,
|
|
104
250
|
courseId: id,
|
|
105
|
-
fields: Object.keys(patch)
|
|
251
|
+
fields: Object.keys(patch),
|
|
252
|
+
actorId: actor.id
|
|
106
253
|
});
|
|
254
|
+
await this.syncThumbnail(course);
|
|
107
255
|
return this.withThumbnail(course);
|
|
108
256
|
}
|
|
109
|
-
async patchCourseSettings(
|
|
257
|
+
async patchCourseSettings(id, value, actor) {
|
|
258
|
+
this.require(actor, "courses:write", id);
|
|
259
|
+
const orgId = actor.orgId;
|
|
110
260
|
const merged = await this.uow.run(async ({ content, outbox }) => {
|
|
111
261
|
const course = await content.findCourseById(orgId, id);
|
|
112
262
|
if (!course) {
|
|
@@ -129,11 +279,14 @@ var ContentService = class {
|
|
|
129
279
|
this.logger.info("course settings patched", {
|
|
130
280
|
orgId,
|
|
131
281
|
courseId: id,
|
|
132
|
-
value
|
|
282
|
+
value,
|
|
283
|
+
actorId: actor.id
|
|
133
284
|
});
|
|
134
285
|
return merged;
|
|
135
286
|
}
|
|
136
|
-
async deleteCourse(
|
|
287
|
+
async deleteCourse(id, actor) {
|
|
288
|
+
this.require(actor, "courses:delete", id);
|
|
289
|
+
const orgId = actor.orgId;
|
|
137
290
|
await this.uow.run(async ({ content, outbox }) => {
|
|
138
291
|
const course = await content.findCourseById(orgId, id);
|
|
139
292
|
if (!course) {
|
|
@@ -152,32 +305,44 @@ var ContentService = class {
|
|
|
152
305
|
});
|
|
153
306
|
this.logger.info("course deleted", {
|
|
154
307
|
orgId,
|
|
155
|
-
courseId: id
|
|
308
|
+
courseId: id,
|
|
309
|
+
actorId: actor.id
|
|
156
310
|
});
|
|
311
|
+
await this.withdrawThumbnail(orgId, id);
|
|
157
312
|
}
|
|
158
|
-
listCourseModules(
|
|
159
|
-
|
|
313
|
+
async listCourseModules(courseId, actor) {
|
|
314
|
+
this.require(actor, "courses:read", courseId);
|
|
315
|
+
return this.repo.listCourseModules(actor.orgId, courseId);
|
|
160
316
|
}
|
|
161
|
-
listCourseActivities(
|
|
162
|
-
|
|
317
|
+
async listCourseActivities(courseId, actor) {
|
|
318
|
+
this.require(actor, "courses:read", courseId);
|
|
319
|
+
return this.repo.listActivitiesForCourse(actor.orgId, courseId);
|
|
163
320
|
}
|
|
164
|
-
async listCourseActivityViews(
|
|
165
|
-
|
|
321
|
+
async listCourseActivityViews(courseId, actor) {
|
|
322
|
+
this.require(actor, "courses:read", courseId);
|
|
323
|
+
const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(actor.orgId, courseId), this.repo.listActivityAssetsForCourse(actor.orgId, courseId)]);
|
|
166
324
|
return activities.map((activity) => ({
|
|
167
325
|
...activity,
|
|
168
326
|
assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
|
|
169
327
|
}));
|
|
170
328
|
}
|
|
171
|
-
listCourseActivityAssets(
|
|
172
|
-
|
|
329
|
+
async listCourseActivityAssets(courseId, actor) {
|
|
330
|
+
this.require(actor, "courses:read", courseId);
|
|
331
|
+
return this.repo.listActivityAssetsForCourse(actor.orgId, courseId);
|
|
173
332
|
}
|
|
174
|
-
getActivity(
|
|
175
|
-
|
|
333
|
+
async getActivity(activityId, actor) {
|
|
334
|
+
const activity = await this.repo.findActivity(actor.orgId, activityId);
|
|
335
|
+
if (activity) this.require(actor, "courses:read", activity.courseId);
|
|
336
|
+
return activity;
|
|
176
337
|
}
|
|
177
|
-
getModule(
|
|
178
|
-
|
|
338
|
+
async getModule(moduleId, actor) {
|
|
339
|
+
const module = await this.repo.findModule(actor.orgId, moduleId);
|
|
340
|
+
if (module) this.require(actor, "courses:read", module.courseId);
|
|
341
|
+
return module;
|
|
179
342
|
}
|
|
180
|
-
async reorderModules(
|
|
343
|
+
async reorderModules(courseId, orderedIds, actor) {
|
|
344
|
+
this.require(actor, "courses:write", courseId);
|
|
345
|
+
const orgId = actor.orgId;
|
|
181
346
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
182
347
|
const reordered = await content.reorderModules(orgId, courseId, orderedIds);
|
|
183
348
|
await outbox.append([contentEvents.modulesReordered.make({
|
|
@@ -193,7 +358,9 @@ var ContentService = class {
|
|
|
193
358
|
});
|
|
194
359
|
return modules;
|
|
195
360
|
}
|
|
196
|
-
async createModule(
|
|
361
|
+
async createModule(courseId, title, actor) {
|
|
362
|
+
this.require(actor, "courses:write", courseId);
|
|
363
|
+
const orgId = actor.orgId;
|
|
197
364
|
const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
|
|
198
365
|
const all = await content.createModule(orgId, courseId, title);
|
|
199
366
|
const module = all.at(-1);
|
|
@@ -211,11 +378,14 @@ var ContentService = class {
|
|
|
211
378
|
orgId,
|
|
212
379
|
courseId,
|
|
213
380
|
moduleId: created.id,
|
|
214
|
-
title
|
|
381
|
+
title,
|
|
382
|
+
actorId: actor.id
|
|
215
383
|
});
|
|
216
384
|
return modules;
|
|
217
385
|
}
|
|
218
|
-
async updateModule(
|
|
386
|
+
async updateModule(courseId, moduleId, title, actor) {
|
|
387
|
+
this.require(actor, "courses:write", courseId);
|
|
388
|
+
const orgId = actor.orgId;
|
|
219
389
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
220
390
|
const existing = await content.findModule(orgId, moduleId);
|
|
221
391
|
if (!existing || existing.courseId !== courseId) {
|
|
@@ -239,11 +409,14 @@ var ContentService = class {
|
|
|
239
409
|
orgId,
|
|
240
410
|
courseId,
|
|
241
411
|
moduleId,
|
|
242
|
-
title
|
|
412
|
+
title,
|
|
413
|
+
actorId: actor.id
|
|
243
414
|
});
|
|
244
415
|
return modules;
|
|
245
416
|
}
|
|
246
|
-
async deleteModule(
|
|
417
|
+
async deleteModule(courseId, moduleId, actor) {
|
|
418
|
+
this.require(actor, "courses:write", courseId);
|
|
419
|
+
const orgId = actor.orgId;
|
|
247
420
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
248
421
|
const module = await content.findModule(orgId, moduleId);
|
|
249
422
|
if (!module || module.courseId !== courseId) {
|
|
@@ -264,11 +437,14 @@ var ContentService = class {
|
|
|
264
437
|
this.logger.info("module deleted", {
|
|
265
438
|
orgId,
|
|
266
439
|
courseId,
|
|
267
|
-
moduleId
|
|
440
|
+
moduleId,
|
|
441
|
+
actorId: actor.id
|
|
268
442
|
});
|
|
269
443
|
return modules;
|
|
270
444
|
}
|
|
271
|
-
async reorderActivities(
|
|
445
|
+
async reorderActivities(courseId, moduleId, orderedIds, actor) {
|
|
446
|
+
this.require(actor, "courses:write", courseId);
|
|
447
|
+
const orgId = actor.orgId;
|
|
272
448
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
273
449
|
const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
|
|
274
450
|
const module = reordered.find((m) => m.id === moduleId);
|
|
@@ -286,21 +462,10 @@ var ContentService = class {
|
|
|
286
462
|
});
|
|
287
463
|
return modules;
|
|
288
464
|
}
|
|
289
|
-
async
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
}
|
|
302
|
-
async saveActivity(orgId, courseId, moduleId, input, activityId) {
|
|
303
|
-
await this.assertLibraryAssets(orgId, input.assetIds ?? []);
|
|
465
|
+
async saveActivity(courseId, moduleId, input, activityId, actor) {
|
|
466
|
+
this.require(actor, "courses:write", courseId);
|
|
467
|
+
const orgId = actor.orgId;
|
|
468
|
+
await this.assertAttachable(actor, courseId, input.assetIds ?? [], () => this.courseAssetIds(orgId, courseId), "An activity");
|
|
304
469
|
const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
|
|
305
470
|
let wasPublished = false;
|
|
306
471
|
if (activityId) {
|
|
@@ -326,7 +491,7 @@ var ContentService = class {
|
|
|
326
491
|
throw new NotFoundError("Module", moduleId);
|
|
327
492
|
}
|
|
328
493
|
}
|
|
329
|
-
const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
|
|
494
|
+
const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId ?? void 0);
|
|
330
495
|
const events = [activityId ? contentEvents.activityUpdated.make({
|
|
331
496
|
orgId,
|
|
332
497
|
data: saved
|
|
@@ -354,11 +519,14 @@ var ContentService = class {
|
|
|
354
519
|
orgId,
|
|
355
520
|
courseId,
|
|
356
521
|
moduleId,
|
|
357
|
-
activityId: activity.id
|
|
522
|
+
activityId: activity.id,
|
|
523
|
+
actorId: actor.id
|
|
358
524
|
});
|
|
359
525
|
return modules;
|
|
360
526
|
}
|
|
361
|
-
async deleteActivity(
|
|
527
|
+
async deleteActivity(courseId, moduleId, activityId, actor) {
|
|
528
|
+
this.require(actor, "courses:write", courseId);
|
|
529
|
+
const orgId = actor.orgId;
|
|
362
530
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
363
531
|
const activity = await content.findActivity(orgId, activityId);
|
|
364
532
|
if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
|
|
@@ -381,18 +549,28 @@ var ContentService = class {
|
|
|
381
549
|
orgId,
|
|
382
550
|
courseId,
|
|
383
551
|
moduleId,
|
|
384
|
-
activityId
|
|
552
|
+
activityId,
|
|
553
|
+
actorId: actor.id
|
|
385
554
|
});
|
|
386
555
|
return modules;
|
|
387
556
|
}
|
|
388
|
-
async listDownloads(
|
|
389
|
-
|
|
557
|
+
async listDownloads(query, actor) {
|
|
558
|
+
const ids = this.visibleIds(actor, ["downloads:read"], "downloads");
|
|
559
|
+
return this.pageWithThumbnails(await this.repo.listDownloads(actor.orgId, query, ids));
|
|
560
|
+
}
|
|
561
|
+
async getDownload(id, actor) {
|
|
562
|
+
const download = await this.findDownload(id, actor);
|
|
563
|
+
if (!download) throw new NotFoundError("Download", id);
|
|
564
|
+
return download;
|
|
390
565
|
}
|
|
391
|
-
async
|
|
392
|
-
|
|
566
|
+
async findDownload(id, actor) {
|
|
567
|
+
this.require(actor, "downloads:read", id);
|
|
568
|
+
const download = await this.repo.getDownload(actor.orgId, id);
|
|
393
569
|
return download ? this.withThumbnail(download) : null;
|
|
394
570
|
}
|
|
395
|
-
async createDownload(
|
|
571
|
+
async createDownload(input, actor) {
|
|
572
|
+
this.require(actor, "downloads:write");
|
|
573
|
+
const orgId = actor.orgId;
|
|
396
574
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
397
575
|
const created = await content.createDownload(orgId, input, slugify(input.title));
|
|
398
576
|
await outbox.append([contentEvents.downloadCreated.make({
|
|
@@ -404,12 +582,15 @@ var ContentService = class {
|
|
|
404
582
|
this.logger.info("download created", {
|
|
405
583
|
orgId,
|
|
406
584
|
downloadId: download.id,
|
|
407
|
-
title: download.title
|
|
585
|
+
title: download.title,
|
|
586
|
+
actorId: actor.id
|
|
408
587
|
});
|
|
409
588
|
return this.withThumbnail(download);
|
|
410
589
|
}
|
|
411
|
-
async updateDownload(
|
|
412
|
-
|
|
590
|
+
async updateDownload(id, patch, actor) {
|
|
591
|
+
this.require(actor, "downloads:write", id);
|
|
592
|
+
const orgId = actor.orgId;
|
|
593
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.downloadAssetIds(orgId, id), "A download");
|
|
413
594
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
414
595
|
const updated = await content.updateDownload(orgId, id, patch);
|
|
415
596
|
if (!updated) {
|
|
@@ -428,11 +609,15 @@ var ContentService = class {
|
|
|
428
609
|
this.logger.info("download updated", {
|
|
429
610
|
orgId,
|
|
430
611
|
downloadId: id,
|
|
431
|
-
fields: Object.keys(patch)
|
|
612
|
+
fields: Object.keys(patch),
|
|
613
|
+
actorId: actor.id
|
|
432
614
|
});
|
|
615
|
+
await this.syncThumbnail(download);
|
|
433
616
|
return this.withThumbnail(download);
|
|
434
617
|
}
|
|
435
|
-
async removeDownload(
|
|
618
|
+
async removeDownload(id, actor) {
|
|
619
|
+
this.require(actor, "downloads:delete", id);
|
|
620
|
+
const orgId = actor.orgId;
|
|
436
621
|
await this.uow.run(async ({ content, outbox }) => {
|
|
437
622
|
const download = await content.getDownload(orgId, id);
|
|
438
623
|
if (!download) {
|
|
@@ -451,14 +636,19 @@ var ContentService = class {
|
|
|
451
636
|
});
|
|
452
637
|
this.logger.info("download deleted", {
|
|
453
638
|
orgId,
|
|
454
|
-
downloadId: id
|
|
639
|
+
downloadId: id,
|
|
640
|
+
actorId: actor.id
|
|
455
641
|
});
|
|
642
|
+
await this.withdrawThumbnail(orgId, id);
|
|
456
643
|
}
|
|
457
|
-
async listDownloadAssets(
|
|
458
|
-
|
|
644
|
+
async listDownloadAssets(downloadId, actor) {
|
|
645
|
+
this.require(actor, "downloads:read", downloadId);
|
|
646
|
+
return this.downloadFiles(actor.orgId, await this.repo.listDownloadAssets(actor.orgId, downloadId));
|
|
459
647
|
}
|
|
460
|
-
async addDownloadAsset(
|
|
461
|
-
|
|
648
|
+
async addDownloadAsset(downloadId, input, actor) {
|
|
649
|
+
this.require(actor, "downloads:write", downloadId);
|
|
650
|
+
const orgId = actor.orgId;
|
|
651
|
+
await this.assertAttachable(actor, downloadId, [input.assetId], () => this.downloadAssetIds(orgId, downloadId), "A download");
|
|
462
652
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
463
653
|
const assets = await content.addDownloadAsset(orgId, downloadId, input);
|
|
464
654
|
await outbox.append([contentEvents.downloadAssetAdded.make({
|
|
@@ -473,11 +663,14 @@ var ContentService = class {
|
|
|
473
663
|
this.logger.info("download asset added", {
|
|
474
664
|
orgId,
|
|
475
665
|
downloadId,
|
|
476
|
-
assetId: input.assetId
|
|
666
|
+
assetId: input.assetId,
|
|
667
|
+
actorId: actor.id
|
|
477
668
|
});
|
|
478
669
|
return this.downloadFiles(orgId, assets);
|
|
479
670
|
}
|
|
480
|
-
async removeDownloadAsset(
|
|
671
|
+
async removeDownloadAsset(downloadId, assetId, actor) {
|
|
672
|
+
this.require(actor, "downloads:write", downloadId);
|
|
673
|
+
const orgId = actor.orgId;
|
|
481
674
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
482
675
|
const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
|
|
483
676
|
await outbox.append([contentEvents.downloadAssetRemoved.make({
|
|
@@ -492,11 +685,14 @@ var ContentService = class {
|
|
|
492
685
|
this.logger.info("download asset removed", {
|
|
493
686
|
orgId,
|
|
494
687
|
downloadId,
|
|
495
|
-
assetId
|
|
688
|
+
assetId,
|
|
689
|
+
actorId: actor.id
|
|
496
690
|
});
|
|
497
691
|
return this.downloadFiles(orgId, assets);
|
|
498
692
|
}
|
|
499
|
-
async renameDownloadAsset(
|
|
693
|
+
async renameDownloadAsset(downloadId, assetId, displayName, actor) {
|
|
694
|
+
this.require(actor, "downloads:write", downloadId);
|
|
695
|
+
const orgId = actor.orgId;
|
|
500
696
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
501
697
|
const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
|
|
502
698
|
await outbox.append([contentEvents.downloadAssetRenamed.make({
|
|
@@ -511,13 +707,16 @@ var ContentService = class {
|
|
|
511
707
|
this.logger.info("download asset renamed", {
|
|
512
708
|
orgId,
|
|
513
709
|
downloadId,
|
|
514
|
-
assetId
|
|
710
|
+
assetId,
|
|
711
|
+
actorId: actor.id
|
|
515
712
|
});
|
|
516
713
|
return this.downloadFiles(orgId, assets);
|
|
517
714
|
}
|
|
518
715
|
/** The caller must send the COMPLETE ordered set — a partial list would
|
|
519
716
|
* silently unlink whatever it omitted. */
|
|
520
|
-
async reorderDownloadAssets(
|
|
717
|
+
async reorderDownloadAssets(downloadId, assetIds, actor) {
|
|
718
|
+
this.require(actor, "downloads:write", downloadId);
|
|
719
|
+
const orgId = actor.orgId;
|
|
521
720
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
522
721
|
const current = await content.listDownloadAssets(orgId, downloadId);
|
|
523
722
|
const currentIds = new Set(current.map((a) => a.assetId));
|
|
@@ -547,13 +746,330 @@ var ContentService = class {
|
|
|
547
746
|
});
|
|
548
747
|
return this.downloadFiles(orgId, assets);
|
|
549
748
|
}
|
|
550
|
-
|
|
551
|
-
|
|
749
|
+
async listEvents(query, actor) {
|
|
750
|
+
const ids = this.visibleIds(actor, ["events:read"], "events");
|
|
751
|
+
return this.pageWithThumbnails(await this.repo.listEvents(actor.orgId, query, ids));
|
|
752
|
+
}
|
|
753
|
+
async getEvent(id, actor) {
|
|
754
|
+
const event = await this.findEvent(id, actor);
|
|
755
|
+
if (!event) throw new NotFoundError("Event", id);
|
|
756
|
+
return event;
|
|
757
|
+
}
|
|
758
|
+
async findEvent(id, actor) {
|
|
759
|
+
this.require(actor, "events:read", id);
|
|
760
|
+
const event = await this.repo.findEventById(actor.orgId, id);
|
|
761
|
+
return event ? this.withThumbnail(event) : null;
|
|
762
|
+
}
|
|
763
|
+
async findEvents(ids, actor) {
|
|
764
|
+
for (const id of ids) this.require(actor, "events:read", id);
|
|
765
|
+
if (ids.length === 0) return [];
|
|
766
|
+
return (await this.repo.findEventsByIds(actor.orgId, ids)).map((event) => this.withThumbnail(event));
|
|
767
|
+
}
|
|
768
|
+
async createEvent(input, actor) {
|
|
769
|
+
this.require(actor, "events:write");
|
|
770
|
+
assertEventTimes(input.startsAt, input.endsAt);
|
|
771
|
+
const orgId = actor.orgId;
|
|
772
|
+
const { settings, ...fields } = input;
|
|
773
|
+
const event = await this.uow.run(async ({ content, outbox }) => {
|
|
774
|
+
const created = await content.createEvent(orgId, fields);
|
|
775
|
+
const event = settings ? {
|
|
776
|
+
...created,
|
|
777
|
+
settings: await content.patchEventSettings(orgId, created.id, settings)
|
|
778
|
+
} : created;
|
|
779
|
+
await outbox.append([contentEvents.eventCreated.make({
|
|
780
|
+
orgId,
|
|
781
|
+
data: event,
|
|
782
|
+
metadata: actorMetadata(actor)
|
|
783
|
+
})]);
|
|
784
|
+
return event;
|
|
785
|
+
});
|
|
786
|
+
this.logger.info("event created", {
|
|
787
|
+
orgId,
|
|
788
|
+
eventId: event.id,
|
|
789
|
+
title: event.title,
|
|
790
|
+
actorId: actor.id
|
|
791
|
+
});
|
|
792
|
+
return this.withThumbnail(event);
|
|
793
|
+
}
|
|
794
|
+
async updateEvent(id, patch, actor) {
|
|
795
|
+
this.require(actor, "events:write", id);
|
|
796
|
+
const orgId = actor.orgId;
|
|
797
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.eventAssetIds(orgId, id), "An event");
|
|
798
|
+
const { settings, ...fields } = patch;
|
|
799
|
+
const { previous, event } = await this.uow.run(async ({ content, outbox }) => {
|
|
800
|
+
const previous = await content.findEventById(orgId, id);
|
|
801
|
+
if (!previous) {
|
|
802
|
+
this.logger.warn("event update rejected — not found", {
|
|
803
|
+
orgId,
|
|
804
|
+
eventId: id
|
|
805
|
+
});
|
|
806
|
+
throw new NotFoundError("Event", id);
|
|
807
|
+
}
|
|
808
|
+
if (previous.status === "canceled") {
|
|
809
|
+
this.logger.warn("event update rejected — canceled", {
|
|
810
|
+
orgId,
|
|
811
|
+
eventId: id
|
|
812
|
+
});
|
|
813
|
+
throw new ConflictError("The event is canceled", "canceled");
|
|
814
|
+
}
|
|
815
|
+
assertEventTimes(fields.startsAt ?? previous.startsAt, fields.endsAt ?? previous.endsAt);
|
|
816
|
+
const updated = await content.updateEvent(orgId, id, fields);
|
|
817
|
+
if (!updated) throw new NotFoundError("Event", id);
|
|
818
|
+
const event = settings ? {
|
|
819
|
+
...updated,
|
|
820
|
+
settings: await content.patchEventSettings(orgId, id, settings)
|
|
821
|
+
} : updated;
|
|
822
|
+
await outbox.append([contentEvents.eventUpdated.make({
|
|
823
|
+
orgId,
|
|
824
|
+
data: event,
|
|
825
|
+
metadata: eventChange(actor, previous, event)
|
|
826
|
+
})]);
|
|
827
|
+
return {
|
|
828
|
+
previous,
|
|
829
|
+
event
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
this.logger.info("event updated", {
|
|
833
|
+
orgId,
|
|
834
|
+
eventId: id,
|
|
835
|
+
fields: Object.keys(patch),
|
|
836
|
+
actorId: actor.id
|
|
837
|
+
});
|
|
838
|
+
await this.scheduleStartingSoon(orgId, previous, event);
|
|
839
|
+
await this.syncThumbnail(event);
|
|
840
|
+
return this.withThumbnail(event);
|
|
841
|
+
}
|
|
842
|
+
async cancelEvent(id, actor) {
|
|
843
|
+
this.require(actor, "events:write", id);
|
|
844
|
+
const orgId = actor.orgId;
|
|
845
|
+
const event = await this.uow.run(async ({ content, outbox }) => {
|
|
846
|
+
const previous = await content.findEventById(orgId, id);
|
|
847
|
+
if (!previous) {
|
|
848
|
+
this.logger.warn("event cancel rejected — not found", {
|
|
849
|
+
orgId,
|
|
850
|
+
eventId: id
|
|
851
|
+
});
|
|
852
|
+
throw new NotFoundError("Event", id);
|
|
853
|
+
}
|
|
854
|
+
if (previous.status === "canceled") {
|
|
855
|
+
this.logger.warn("event cancel rejected — already canceled", {
|
|
856
|
+
orgId,
|
|
857
|
+
eventId: id
|
|
858
|
+
});
|
|
859
|
+
throw new ConflictError("The event is already canceled", "canceled");
|
|
860
|
+
}
|
|
861
|
+
const updated = await content.updateEvent(orgId, id, { status: "canceled" });
|
|
862
|
+
if (!updated) throw new NotFoundError("Event", id);
|
|
863
|
+
await outbox.append([contentEvents.eventUpdated.make({
|
|
864
|
+
orgId,
|
|
865
|
+
data: updated,
|
|
866
|
+
metadata: eventChange(actor, previous, updated)
|
|
867
|
+
})]);
|
|
868
|
+
return updated;
|
|
869
|
+
});
|
|
870
|
+
this.logger.info("event canceled", {
|
|
871
|
+
orgId,
|
|
872
|
+
eventId: id,
|
|
873
|
+
actorId: actor.id
|
|
874
|
+
});
|
|
875
|
+
await this.cancelStartingSoon(orgId, id);
|
|
876
|
+
await this.syncThumbnail(event);
|
|
877
|
+
return this.withThumbnail(event);
|
|
878
|
+
}
|
|
879
|
+
async deleteEvent(id, actor) {
|
|
880
|
+
this.require(actor, "events:delete", id);
|
|
881
|
+
const orgId = actor.orgId;
|
|
882
|
+
await this.uow.run(async ({ content, outbox }) => {
|
|
883
|
+
const event = await content.findEventById(orgId, id);
|
|
884
|
+
if (!event) {
|
|
885
|
+
this.logger.warn("event delete rejected — not found", {
|
|
886
|
+
orgId,
|
|
887
|
+
eventId: id
|
|
888
|
+
});
|
|
889
|
+
throw new NotFoundError("Event", id);
|
|
890
|
+
}
|
|
891
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
892
|
+
if (!await content.deleteEvent(orgId, id)) throw new NotFoundError("Event", id);
|
|
893
|
+
await content.deleteEventSettings(orgId, id);
|
|
894
|
+
await outbox.append([contentEvents.eventDeleted.make({
|
|
895
|
+
orgId,
|
|
896
|
+
data: event,
|
|
897
|
+
metadata: actorMetadata(actor)
|
|
898
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
899
|
+
});
|
|
900
|
+
this.logger.info("event deleted", {
|
|
901
|
+
orgId,
|
|
902
|
+
eventId: id,
|
|
903
|
+
actorId: actor.id
|
|
904
|
+
});
|
|
905
|
+
await this.withdrawThumbnail(orgId, id);
|
|
906
|
+
await this.cancelStartingSoon(orgId, id);
|
|
907
|
+
}
|
|
908
|
+
async startingSoon(orgId, eventId, lead, now) {
|
|
909
|
+
const event = await this.repo.findEventById(orgId, eventId);
|
|
910
|
+
if (!event || event.status !== "published") {
|
|
911
|
+
this.logger.debug("event no longer published", {
|
|
912
|
+
orgId,
|
|
913
|
+
eventId,
|
|
914
|
+
lead
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const due = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
|
|
919
|
+
if (due > now) {
|
|
920
|
+
await this.scheduler.at(EVENT_STARTING_SOON_JOB, due, startingSoonKey(eventId, lead), {
|
|
921
|
+
orgId,
|
|
922
|
+
eventId,
|
|
923
|
+
lead
|
|
924
|
+
});
|
|
925
|
+
this.logger.info("event starting soon timer moved", {
|
|
926
|
+
orgId,
|
|
927
|
+
eventId,
|
|
928
|
+
lead,
|
|
929
|
+
due: due.toISOString()
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
await this.uow.run(({ outbox }) => outbox.append([contentEvents.eventStartingSoon.make({
|
|
934
|
+
orgId,
|
|
935
|
+
data: {
|
|
936
|
+
eventId,
|
|
937
|
+
lead,
|
|
938
|
+
startsAt: event.startsAt
|
|
939
|
+
}
|
|
940
|
+
})]));
|
|
941
|
+
this.logger.info("event starting soon", {
|
|
942
|
+
orgId,
|
|
943
|
+
eventId,
|
|
944
|
+
lead
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async scheduleStartingSoon(orgId, previous, event) {
|
|
948
|
+
if (event.status !== "published") {
|
|
949
|
+
if (previous.status === "published") await this.cancelStartingSoon(orgId, event.id);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (previous.status === "published" && previous.startsAt.getTime() === event.startsAt.getTime()) return;
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
for (const lead of EVENT_LEAD_KEYS) {
|
|
955
|
+
const key = startingSoonKey(event.id, lead);
|
|
956
|
+
const runAt = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
|
|
957
|
+
try {
|
|
958
|
+
if (runAt.getTime() > now) await this.scheduler.at(EVENT_STARTING_SOON_JOB, runAt, key, {
|
|
959
|
+
orgId,
|
|
960
|
+
eventId: event.id,
|
|
961
|
+
lead
|
|
962
|
+
});
|
|
963
|
+
else await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, key);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
this.logger.error("event starting soon timer not set", {
|
|
966
|
+
orgId,
|
|
967
|
+
eventId: event.id,
|
|
968
|
+
lead,
|
|
969
|
+
err
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
552
973
|
}
|
|
553
|
-
|
|
554
|
-
|
|
974
|
+
async cancelStartingSoon(orgId, eventId) {
|
|
975
|
+
for (const lead of EVENT_LEAD_KEYS) try {
|
|
976
|
+
await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, startingSoonKey(eventId, lead));
|
|
977
|
+
} catch (err) {
|
|
978
|
+
this.logger.warn("event starting soon timer not cancelled", {
|
|
979
|
+
orgId,
|
|
980
|
+
eventId,
|
|
981
|
+
lead,
|
|
982
|
+
err
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async listEventAssets(eventId, actor) {
|
|
987
|
+
this.require(actor, "events:read", eventId);
|
|
988
|
+
const orgId = actor.orgId;
|
|
989
|
+
if (!await this.repo.findEventById(orgId, eventId)) throw new NotFoundError("Event", eventId);
|
|
990
|
+
return this.eventFiles(orgId, await this.repo.listEventAssets(orgId, eventId));
|
|
991
|
+
}
|
|
992
|
+
async eventFilesFor(eventIds, actor) {
|
|
993
|
+
for (const id of eventIds) this.require(actor, "events:read", id);
|
|
994
|
+
const byEvent = Object.fromEntries(eventIds.map((id) => [id, []]));
|
|
995
|
+
if (eventIds.length === 0) return byEvent;
|
|
996
|
+
const files = await this.eventFiles(actor.orgId, await this.repo.listEventAssetsFor(actor.orgId, eventIds));
|
|
997
|
+
for (const file of files) byEvent[file.eventId]?.push(file);
|
|
998
|
+
return byEvent;
|
|
999
|
+
}
|
|
1000
|
+
async addEventAsset(eventId, input, actor) {
|
|
1001
|
+
this.require(actor, "events:write", eventId);
|
|
1002
|
+
const orgId = actor.orgId;
|
|
1003
|
+
await this.assertAttachable(actor, eventId, [input.assetId], () => this.eventAssetIds(orgId, eventId), "An event");
|
|
1004
|
+
const assets = await this.uow.run(({ content }) => content.addEventAsset(orgId, eventId, input));
|
|
1005
|
+
this.logger.info("event asset added", {
|
|
1006
|
+
orgId,
|
|
1007
|
+
eventId,
|
|
1008
|
+
assetId: input.assetId,
|
|
1009
|
+
actorId: actor.id
|
|
1010
|
+
});
|
|
1011
|
+
return this.eventFiles(orgId, assets);
|
|
1012
|
+
}
|
|
1013
|
+
async removeEventAsset(eventId, assetId, actor) {
|
|
1014
|
+
this.require(actor, "events:write", eventId);
|
|
1015
|
+
const orgId = actor.orgId;
|
|
1016
|
+
const assets = await this.uow.run(({ content }) => content.removeEventAsset(orgId, eventId, assetId));
|
|
1017
|
+
this.logger.info("event asset removed", {
|
|
1018
|
+
orgId,
|
|
1019
|
+
eventId,
|
|
1020
|
+
assetId,
|
|
1021
|
+
actorId: actor.id
|
|
1022
|
+
});
|
|
1023
|
+
return this.eventFiles(orgId, assets);
|
|
1024
|
+
}
|
|
1025
|
+
async renameEventAsset(eventId, assetId, displayName, actor) {
|
|
1026
|
+
this.require(actor, "events:write", eventId);
|
|
1027
|
+
const orgId = actor.orgId;
|
|
1028
|
+
const assets = await this.uow.run(({ content }) => content.renameEventAsset(orgId, eventId, assetId, displayName));
|
|
1029
|
+
this.logger.info("event asset renamed", {
|
|
1030
|
+
orgId,
|
|
1031
|
+
eventId,
|
|
1032
|
+
assetId,
|
|
1033
|
+
actorId: actor.id
|
|
1034
|
+
});
|
|
1035
|
+
return this.eventFiles(orgId, assets);
|
|
1036
|
+
}
|
|
1037
|
+
async reorderEventAssets(eventId, assetIds, actor) {
|
|
1038
|
+
this.require(actor, "events:write", eventId);
|
|
1039
|
+
const orgId = actor.orgId;
|
|
1040
|
+
const assets = await this.uow.run(async ({ content }) => {
|
|
1041
|
+
if (!sameIdSet((await content.listEventAssets(orgId, eventId)).map((link) => link.assetId), assetIds)) {
|
|
1042
|
+
this.logger.warn("event asset reorder rejected — id set mismatch", {
|
|
1043
|
+
orgId,
|
|
1044
|
+
eventId,
|
|
1045
|
+
assetIds
|
|
1046
|
+
});
|
|
1047
|
+
throw new ConflictError("Ordered asset ids does not match the event's current assets");
|
|
1048
|
+
}
|
|
1049
|
+
return content.reorderEventAssets(orgId, eventId, assetIds);
|
|
1050
|
+
});
|
|
1051
|
+
this.logger.debug("event assets reordered", {
|
|
1052
|
+
orgId,
|
|
1053
|
+
eventId
|
|
1054
|
+
});
|
|
1055
|
+
return this.eventFiles(orgId, assets);
|
|
1056
|
+
}
|
|
1057
|
+
async listBundles(query, actor) {
|
|
1058
|
+
this.require(actor, "bundles:read");
|
|
1059
|
+
return this.repo.listBundles(actor.orgId, query);
|
|
555
1060
|
}
|
|
556
|
-
async
|
|
1061
|
+
async getBundle(id, actor) {
|
|
1062
|
+
const bundle = await this.findBundle(id, actor);
|
|
1063
|
+
if (!bundle) throw new NotFoundError("Bundle", id);
|
|
1064
|
+
return bundle;
|
|
1065
|
+
}
|
|
1066
|
+
async findBundle(id, actor) {
|
|
1067
|
+
this.require(actor, "bundles:read");
|
|
1068
|
+
return this.repo.findBundleById(actor.orgId, id);
|
|
1069
|
+
}
|
|
1070
|
+
async createBundle(input, actor) {
|
|
1071
|
+
this.require(actor, "bundles:write");
|
|
1072
|
+
const orgId = actor.orgId;
|
|
557
1073
|
const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
|
|
558
1074
|
const created = await content.createBundle(orgId, input);
|
|
559
1075
|
const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
|
|
@@ -576,11 +1092,14 @@ var ContentService = class {
|
|
|
576
1092
|
orgId,
|
|
577
1093
|
bundleId: bundle.id,
|
|
578
1094
|
name: bundle.name,
|
|
579
|
-
itemCount: items.length
|
|
1095
|
+
itemCount: items.length,
|
|
1096
|
+
actorId: actor.id
|
|
580
1097
|
});
|
|
581
1098
|
return bundle;
|
|
582
1099
|
}
|
|
583
|
-
async updateBundle(
|
|
1100
|
+
async updateBundle(id, patch, actor) {
|
|
1101
|
+
this.require(actor, "bundles:write");
|
|
1102
|
+
const orgId = actor.orgId;
|
|
584
1103
|
const bundle = await this.uow.run(async ({ content, outbox }) => {
|
|
585
1104
|
const updated = await content.updateBundle(orgId, id, patch);
|
|
586
1105
|
if (!updated) {
|
|
@@ -599,11 +1118,14 @@ var ContentService = class {
|
|
|
599
1118
|
this.logger.info("bundle updated", {
|
|
600
1119
|
orgId,
|
|
601
1120
|
bundleId: id,
|
|
602
|
-
fields: Object.keys(patch)
|
|
1121
|
+
fields: Object.keys(patch),
|
|
1122
|
+
actorId: actor.id
|
|
603
1123
|
});
|
|
604
1124
|
return bundle;
|
|
605
1125
|
}
|
|
606
|
-
async deleteBundle(
|
|
1126
|
+
async deleteBundle(id, actor) {
|
|
1127
|
+
this.require(actor, "bundles:delete");
|
|
1128
|
+
const orgId = actor.orgId;
|
|
607
1129
|
await this.uow.run(async ({ content, outbox }) => {
|
|
608
1130
|
const bundle = await content.findBundleById(orgId, id);
|
|
609
1131
|
if (!bundle) {
|
|
@@ -621,13 +1143,17 @@ var ContentService = class {
|
|
|
621
1143
|
});
|
|
622
1144
|
this.logger.info("bundle deleted", {
|
|
623
1145
|
orgId,
|
|
624
|
-
bundleId: id
|
|
1146
|
+
bundleId: id,
|
|
1147
|
+
actorId: actor.id
|
|
625
1148
|
});
|
|
626
1149
|
}
|
|
627
|
-
listBundleItems(
|
|
628
|
-
|
|
1150
|
+
async listBundleItems(bundleId, actor) {
|
|
1151
|
+
this.require(actor, "bundles:read");
|
|
1152
|
+
return this.repo.listBundleItems(actor.orgId, bundleId);
|
|
629
1153
|
}
|
|
630
|
-
async addBundleItem(
|
|
1154
|
+
async addBundleItem(bundleId, contentId, actor) {
|
|
1155
|
+
this.require(actor, "bundles:write");
|
|
1156
|
+
const orgId = actor.orgId;
|
|
631
1157
|
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
632
1158
|
const all = await content.addBundleItem(orgId, bundleId, contentId);
|
|
633
1159
|
await outbox.append([contentEvents.bundleItemAdded.make({
|
|
@@ -642,11 +1168,14 @@ var ContentService = class {
|
|
|
642
1168
|
this.logger.info("bundle item added", {
|
|
643
1169
|
orgId,
|
|
644
1170
|
bundleId,
|
|
645
|
-
contentId
|
|
1171
|
+
contentId,
|
|
1172
|
+
actorId: actor.id
|
|
646
1173
|
});
|
|
647
1174
|
return items;
|
|
648
1175
|
}
|
|
649
|
-
async removeBundleItem(
|
|
1176
|
+
async removeBundleItem(bundleId, contentId, actor) {
|
|
1177
|
+
this.require(actor, "bundles:write");
|
|
1178
|
+
const orgId = actor.orgId;
|
|
650
1179
|
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
651
1180
|
const all = await content.removeBundleItem(orgId, bundleId, contentId);
|
|
652
1181
|
await outbox.append([contentEvents.bundleItemRemoved.make({
|
|
@@ -661,7 +1190,8 @@ var ContentService = class {
|
|
|
661
1190
|
this.logger.info("bundle item removed", {
|
|
662
1191
|
orgId,
|
|
663
1192
|
bundleId,
|
|
664
|
-
contentId
|
|
1193
|
+
contentId,
|
|
1194
|
+
actorId: actor.id
|
|
665
1195
|
});
|
|
666
1196
|
return items;
|
|
667
1197
|
}
|
|
@@ -694,35 +1224,60 @@ var ContentService = class {
|
|
|
694
1224
|
settings
|
|
695
1225
|
};
|
|
696
1226
|
}
|
|
697
|
-
async listCommunities(
|
|
698
|
-
|
|
1227
|
+
async listCommunities(query, actor) {
|
|
1228
|
+
const ids = this.visibleIds(actor, COMMUNITY_READERS, "communities");
|
|
1229
|
+
return this.pageWithThumbnails(await this.repo.listCommunities(actor.orgId, query, ids));
|
|
699
1230
|
}
|
|
700
|
-
async getCommunity(
|
|
701
|
-
const community = await this.
|
|
1231
|
+
async getCommunity(id, actor) {
|
|
1232
|
+
const community = await this.findCommunity(id, actor);
|
|
1233
|
+
if (!community) throw new NotFoundError("Community", id);
|
|
1234
|
+
return community;
|
|
1235
|
+
}
|
|
1236
|
+
async findCommunity(id, actor) {
|
|
1237
|
+
this.requireAny(actor, COMMUNITY_READERS, id);
|
|
1238
|
+
const community = await this.repo.findCommunityById(actor.orgId, id);
|
|
702
1239
|
return community ? this.withThumbnail(community) : null;
|
|
703
1240
|
}
|
|
704
1241
|
communitiesWithDigest() {
|
|
705
1242
|
return this.repo.communitiesWithDigest();
|
|
706
1243
|
}
|
|
707
|
-
listCommunitiesById(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1244
|
+
async listCommunitiesById(ids, actor) {
|
|
1245
|
+
if (!actor.permissions.anywhere("communities:read")) {
|
|
1246
|
+
this.logger.warn("content list refused", {
|
|
1247
|
+
orgId: actor.orgId,
|
|
1248
|
+
actorId: actor.id,
|
|
1249
|
+
permission: "communities:read"
|
|
1250
|
+
});
|
|
1251
|
+
throw new ForbiddenError("You cannot see communities");
|
|
1252
|
+
}
|
|
1253
|
+
const visible = ids.filter((id) => actor.permissions.has("communities:read", id));
|
|
1254
|
+
return visible.length ? this.repo.findCommunitiesByIds(actor.orgId, visible) : [];
|
|
1255
|
+
}
|
|
1256
|
+
async getSpace(spaceId, actor) {
|
|
1257
|
+
const space = await this.repo.findSpace(actor.orgId, spaceId);
|
|
1258
|
+
if (!space) return null;
|
|
1259
|
+
this.require(actor, "communities:read", space.communityId);
|
|
1260
|
+
return this.oneSpaceView(space);
|
|
1261
|
+
}
|
|
1262
|
+
async getCommunitySpace(communityId, spaceId, actor) {
|
|
1263
|
+
this.require(actor, "communities:read", communityId);
|
|
1264
|
+
const space = await this.repo.findSpace(actor.orgId, spaceId);
|
|
1265
|
+
if (space?.communityId !== communityId) throw new NotFoundError("Space", spaceId);
|
|
1266
|
+
return this.oneSpaceView(space);
|
|
1267
|
+
}
|
|
1268
|
+
async listSpaces(communityId, actor) {
|
|
1269
|
+
this.requireAny(actor, COMMUNITY_READERS, communityId);
|
|
1270
|
+
const community = await this.repo.findCommunityById(actor.orgId, communityId);
|
|
1271
|
+
if (!community) throw new NotFoundError("Community", communityId);
|
|
1272
|
+
return (await this.repo.listSpaces(actor.orgId, communityId)).map((space) => this.spaceView(space, community.status === "published"));
|
|
1273
|
+
}
|
|
1274
|
+
async getDmSettings(communityId, actor) {
|
|
1275
|
+
this.require(actor, "communities:read", communityId);
|
|
1276
|
+
return (await this.repo.findCommunityById(actor.orgId, communityId))?.settings.dm ?? null;
|
|
1277
|
+
}
|
|
1278
|
+
async createCommunity(input, actor) {
|
|
1279
|
+
this.require(actor, "communities:write");
|
|
1280
|
+
const orgId = actor.orgId;
|
|
726
1281
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
727
1282
|
const created = await content.createCommunity(orgId, input, slugify(input.title));
|
|
728
1283
|
await outbox.append([contentEvents.communityCreated.make({
|
|
@@ -739,9 +1294,10 @@ var ContentService = class {
|
|
|
739
1294
|
});
|
|
740
1295
|
return this.withThumbnail(community);
|
|
741
1296
|
}
|
|
742
|
-
async updateCommunity(
|
|
743
|
-
|
|
744
|
-
|
|
1297
|
+
async updateCommunity(id, patch, actor) {
|
|
1298
|
+
this.require(actor, "communities:write", id);
|
|
1299
|
+
const orgId = actor.orgId;
|
|
1300
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], async () => [(await this.repo.findCommunityById(orgId, id))?.thumbnailAssetId ?? null], "A community");
|
|
745
1301
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
746
1302
|
const updated = await content.updateCommunity(orgId, id, patch);
|
|
747
1303
|
if (!updated) {
|
|
@@ -764,17 +1320,23 @@ var ContentService = class {
|
|
|
764
1320
|
communityId: id,
|
|
765
1321
|
actorId: actor.id
|
|
766
1322
|
});
|
|
1323
|
+
await this.syncThumbnail(community);
|
|
1324
|
+
if (patch.status !== void 0) {
|
|
1325
|
+
const spaces = await this.repo.listSpaces(orgId, id);
|
|
1326
|
+
await Promise.all(spaces.map((space) => this.syncSpaceImages(space, community.status === "published")));
|
|
1327
|
+
}
|
|
767
1328
|
return this.withThumbnail(community);
|
|
768
1329
|
}
|
|
769
|
-
publishCommunity(
|
|
770
|
-
return this.updateCommunity(
|
|
1330
|
+
publishCommunity(id, actor) {
|
|
1331
|
+
return this.updateCommunity(id, { status: "published" }, actor);
|
|
771
1332
|
}
|
|
772
|
-
unpublishCommunity(
|
|
773
|
-
return this.updateCommunity(
|
|
1333
|
+
unpublishCommunity(id, actor) {
|
|
1334
|
+
return this.updateCommunity(id, { status: "draft" }, actor);
|
|
774
1335
|
}
|
|
775
|
-
async deleteCommunity(
|
|
776
|
-
|
|
777
|
-
|
|
1336
|
+
async deleteCommunity(id, actor) {
|
|
1337
|
+
this.require(actor, "communities:delete", id);
|
|
1338
|
+
const orgId = actor.orgId;
|
|
1339
|
+
const spaceIds = await this.uow.run(async ({ content, outbox }) => {
|
|
778
1340
|
const community = await content.findCommunityById(orgId, id);
|
|
779
1341
|
if (!community) {
|
|
780
1342
|
this.logger.warn("community delete rejected — not found", {
|
|
@@ -785,6 +1347,7 @@ var ContentService = class {
|
|
|
785
1347
|
throw new NotFoundError("Community", id);
|
|
786
1348
|
}
|
|
787
1349
|
const linked = await content.spacesLinkingContent(orgId, id);
|
|
1350
|
+
const own = await content.listSpaces(orgId, id);
|
|
788
1351
|
if (!await content.deleteCommunity(orgId, id)) throw new NotFoundError("Community", id);
|
|
789
1352
|
await content.deleteCommunitySettings(orgId, id);
|
|
790
1353
|
await outbox.append([contentEvents.communityDeleted.make({
|
|
@@ -792,15 +1355,19 @@ var ContentService = class {
|
|
|
792
1355
|
data: community,
|
|
793
1356
|
metadata: actorMetadata(actor)
|
|
794
1357
|
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
1358
|
+
return own.map((space) => space.id);
|
|
795
1359
|
});
|
|
796
1360
|
this.logger.info("community deleted", {
|
|
797
1361
|
orgId,
|
|
798
1362
|
communityId: id,
|
|
799
1363
|
actorId: actor.id
|
|
800
1364
|
});
|
|
1365
|
+
await this.withdrawThumbnail(orgId, id);
|
|
1366
|
+
await Promise.all(spaceIds.map((spaceId) => this.files.syncPublicImages(orgId, spaceId, [])));
|
|
801
1367
|
}
|
|
802
|
-
async patchCommunitySettings(
|
|
803
|
-
|
|
1368
|
+
async patchCommunitySettings(id, patch, actor) {
|
|
1369
|
+
this.require(actor, "communities:write", id);
|
|
1370
|
+
const orgId = actor.orgId;
|
|
804
1371
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
805
1372
|
const updated = await content.patchCommunitySettings(orgId, id, patch);
|
|
806
1373
|
if (!updated) {
|
|
@@ -825,8 +1392,9 @@ var ContentService = class {
|
|
|
825
1392
|
});
|
|
826
1393
|
return this.withThumbnail(community);
|
|
827
1394
|
}
|
|
828
|
-
async createSpace(
|
|
829
|
-
|
|
1395
|
+
async createSpace(communityId, input, actor) {
|
|
1396
|
+
this.require(actor, "communities:write", communityId);
|
|
1397
|
+
const orgId = actor.orgId;
|
|
830
1398
|
const resolved = this.resolvedSpace(input);
|
|
831
1399
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
832
1400
|
if (!await content.findCommunityById(orgId, communityId)) {
|
|
@@ -856,10 +1424,19 @@ var ContentService = class {
|
|
|
856
1424
|
spaceId: space.id,
|
|
857
1425
|
actorId: actor.id
|
|
858
1426
|
});
|
|
859
|
-
return space;
|
|
860
|
-
}
|
|
861
|
-
async updateSpace(
|
|
862
|
-
|
|
1427
|
+
return this.oneSpaceView(space);
|
|
1428
|
+
}
|
|
1429
|
+
async updateSpace(communityId, spaceId, patch, actor) {
|
|
1430
|
+
this.require(actor, "communities:write", communityId);
|
|
1431
|
+
const orgId = actor.orgId;
|
|
1432
|
+
await this.assertAttachable(actor, communityId, [
|
|
1433
|
+
patch.coverAssetId,
|
|
1434
|
+
patch.backgroundAssetId,
|
|
1435
|
+
patch.iconAssetId
|
|
1436
|
+
], async () => {
|
|
1437
|
+
const current = await this.repo.findSpace(orgId, spaceId);
|
|
1438
|
+
return current ? spaceImageIds(current) : [];
|
|
1439
|
+
}, "A space");
|
|
863
1440
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
864
1441
|
const current = await content.findSpace(orgId, spaceId);
|
|
865
1442
|
if (!current || current.communityId !== communityId) {
|
|
@@ -892,10 +1469,13 @@ var ContentService = class {
|
|
|
892
1469
|
spaceId,
|
|
893
1470
|
actorId: actor.id
|
|
894
1471
|
});
|
|
895
|
-
|
|
1472
|
+
const published = await this.communityPublished(orgId, communityId);
|
|
1473
|
+
await this.syncSpaceImages(space, published);
|
|
1474
|
+
return this.spaceView(space, published);
|
|
896
1475
|
}
|
|
897
|
-
async deleteSpace(
|
|
898
|
-
|
|
1476
|
+
async deleteSpace(communityId, spaceId, actor) {
|
|
1477
|
+
this.require(actor, "communities:write", communityId);
|
|
1478
|
+
const orgId = actor.orgId;
|
|
899
1479
|
await this.uow.run(async ({ content, outbox }) => {
|
|
900
1480
|
const space = await content.findSpace(orgId, spaceId);
|
|
901
1481
|
if (!space || space.communityId !== communityId) {
|
|
@@ -920,9 +1500,11 @@ var ContentService = class {
|
|
|
920
1500
|
spaceId,
|
|
921
1501
|
actorId: actor.id
|
|
922
1502
|
});
|
|
1503
|
+
await this.files.syncPublicImages(orgId, spaceId, []);
|
|
923
1504
|
}
|
|
924
|
-
async reorderSpaces(
|
|
925
|
-
|
|
1505
|
+
async reorderSpaces(communityId, orderedIds, actor) {
|
|
1506
|
+
this.require(actor, "communities:write", communityId);
|
|
1507
|
+
const orgId = actor.orgId;
|
|
926
1508
|
const spaces = await this.uow.run(async ({ content, outbox }) => {
|
|
927
1509
|
if (!await content.findCommunityById(orgId, communityId)) {
|
|
928
1510
|
this.logger.warn("spaces reorder rejected — community not found", {
|
|
@@ -946,10 +1528,12 @@ var ContentService = class {
|
|
|
946
1528
|
communityId,
|
|
947
1529
|
actorId: actor.id
|
|
948
1530
|
});
|
|
949
|
-
|
|
1531
|
+
const published = await this.communityPublished(orgId, communityId);
|
|
1532
|
+
return spaces.map((space) => this.spaceView(space, published));
|
|
950
1533
|
}
|
|
951
|
-
async setSpaceTopics(
|
|
952
|
-
|
|
1534
|
+
async setSpaceTopics(communityId, spaceId, input, actor) {
|
|
1535
|
+
this.require(actor, "communities:write", communityId);
|
|
1536
|
+
const orgId = actor.orgId;
|
|
953
1537
|
const names = input.topics.map((topic) => topic.name);
|
|
954
1538
|
if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
|
|
955
1539
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -982,10 +1566,11 @@ var ContentService = class {
|
|
|
982
1566
|
spaceId,
|
|
983
1567
|
actorId: actor.id
|
|
984
1568
|
});
|
|
985
|
-
return space;
|
|
1569
|
+
return this.oneSpaceView(space);
|
|
986
1570
|
}
|
|
987
|
-
async addSpaceLink(
|
|
988
|
-
|
|
1571
|
+
async addSpaceLink(communityId, spaceId, contentId, actor) {
|
|
1572
|
+
this.require(actor, "communities:write", communityId);
|
|
1573
|
+
const orgId = actor.orgId;
|
|
989
1574
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
990
1575
|
const current = await content.findSpace(orgId, spaceId);
|
|
991
1576
|
if (!current || current.communityId !== communityId) {
|
|
@@ -1013,10 +1598,11 @@ var ContentService = class {
|
|
|
1013
1598
|
contentId,
|
|
1014
1599
|
actorId: actor.id
|
|
1015
1600
|
});
|
|
1016
|
-
return space;
|
|
1601
|
+
return this.oneSpaceView(space);
|
|
1017
1602
|
}
|
|
1018
|
-
async removeSpaceLink(
|
|
1019
|
-
|
|
1603
|
+
async removeSpaceLink(communityId, spaceId, linkId, actor) {
|
|
1604
|
+
this.require(actor, "communities:write", communityId);
|
|
1605
|
+
const orgId = actor.orgId;
|
|
1020
1606
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1021
1607
|
const current = await content.findSpace(orgId, spaceId);
|
|
1022
1608
|
if (!current || current.communityId !== communityId) {
|
|
@@ -1043,10 +1629,11 @@ var ContentService = class {
|
|
|
1043
1629
|
linkId,
|
|
1044
1630
|
actorId: actor.id
|
|
1045
1631
|
});
|
|
1046
|
-
return space;
|
|
1632
|
+
return this.oneSpaceView(space);
|
|
1047
1633
|
}
|
|
1048
|
-
async reorderSpaceLinks(
|
|
1049
|
-
|
|
1634
|
+
async reorderSpaceLinks(communityId, spaceId, orderedIds, actor) {
|
|
1635
|
+
this.require(actor, "communities:write", communityId);
|
|
1636
|
+
const orgId = actor.orgId;
|
|
1050
1637
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1051
1638
|
const current = await content.findSpace(orgId, spaceId);
|
|
1052
1639
|
if (!current || current.communityId !== communityId) {
|
|
@@ -1073,7 +1660,7 @@ var ContentService = class {
|
|
|
1073
1660
|
spaceId,
|
|
1074
1661
|
actorId: actor.id
|
|
1075
1662
|
});
|
|
1076
|
-
return space;
|
|
1663
|
+
return this.oneSpaceView(space);
|
|
1077
1664
|
}
|
|
1078
1665
|
};
|
|
1079
1666
|
//#endregion
|