@headless-lms/core 0.3.0 → 0.5.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 +24 -18
- package/dist/assets/file-urls.d.ts +11 -7
- package/dist/assets/file-urls.js +14 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +4 -3
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +37 -17
- package/dist/assets/service.d.ts +30 -15
- package/dist/assets/service.js +229 -59
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +137 -65
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +20 -20
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +36 -20
- package/dist/automations/community-mail.js +32 -24
- 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/events.d.ts +0 -10
- package/dist/automations/index.d.ts +12 -4
- package/dist/automations/index.js +9 -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.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +346 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +96 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +276 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- 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/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +35 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +98 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +29 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +560 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +134 -21
- package/dist/automations/service.d.ts +30 -30
- package/dist/automations/service.js +111 -144
- 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 +437 -46
- package/dist/content/events.js +45 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +8 -4
- package/dist/content/index.js +5 -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 +37 -0
- package/dist/content/notification-categories.js +73 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +132 -68
- package/dist/content/service.d.ts +100 -68
- package/dist/content/service.js +703 -146
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +25 -0
- package/dist/content/titles.js +82 -0
- package/dist/content/titles.js.map +1 -0
- 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 +63 -39
- package/dist/discussion/chats.js +455 -190
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +227 -23
- package/dist/discussion/events.js +114 -19
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -4
- package/dist/discussion/index.js +9 -2
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +279 -114
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +60 -44
- package/dist/discussion/posts.js +453 -287
- 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 +50 -35
- package/dist/discussion/service.js +333 -205
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +115 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +28 -0
- package/dist/discussion/titles.js +98 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +3 -2
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +68 -38
- 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 +43 -28
- package/dist/entitlements/service.js +387 -160
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +109 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/index.d.ts +6 -5
- package/dist/identity/index.js +4 -3
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +114 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +80 -56
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +17 -13
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +33 -16
- 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 +45 -0
- package/dist/organizations/effective-permissions.js +113 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +320 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +15 -10
- package/dist/organizations/index.js +8 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- 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 +34 -0
- package/dist/organizations/notification-categories.js +66 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- 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 +104 -35
- package/dist/organizations/service.js +1247 -323
- 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/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 +23 -8
- 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 +80 -26
- package/dist/reporting/member/ports.d.ts +50 -28
- package/dist/reporting/member/service.d.ts +81 -35
- package/dist/reporting/member/service.js +438 -145
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +22 -3
- package/dist/shared/errors.d.ts +15 -2
- package/dist/shared/errors.js +21 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +7 -0
- package/dist/shared/id.js +8 -1
- 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/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +68 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +119 -6
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +14 -17
- package/dist/types/index.d.ts +13 -12
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- 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/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +596 -11
- package/dist/types/schemas/content.js +225 -9
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +362 -66
- package/dist/types/schemas/discussion.js +123 -15
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +14 -1
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -7
- package/dist/types/schemas/index.js +8 -7
- package/dist/types/schemas/notifications.d.ts +96 -0
- package/dist/types/schemas/notifications.js +57 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +96 -60
- package/dist/types/schemas/organizations.js +78 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +0 -1
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -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
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
import { spaceSettingsDefaults } from "../types/schemas/content.js";
|
|
2
|
-
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
3
2
|
import { noopLogger } from "../shared/logger.js";
|
|
3
|
+
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.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) {
|
|
8
10
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
9
11
|
}
|
|
12
|
+
function isPublishedActivity(settings) {
|
|
13
|
+
return settings?.published !== false;
|
|
14
|
+
}
|
|
10
15
|
function actorMetadata(actor) {
|
|
11
|
-
return {
|
|
16
|
+
return { actorId: actor.id };
|
|
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}`;
|
|
12
34
|
}
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
const EVENT_LEAD_KEYS = Object.keys(EVENT_LEADS);
|
|
36
|
+
function shownPublicly(item) {
|
|
37
|
+
return item.status === "published" || item.type === "event" && item.status === "canceled";
|
|
15
38
|
}
|
|
16
39
|
function sameIdSet(current, requested) {
|
|
17
40
|
if (current.length !== requested.length) return false;
|
|
@@ -24,6 +47,7 @@ var ContentService = class {
|
|
|
24
47
|
libraryAssets;
|
|
25
48
|
files;
|
|
26
49
|
fileUrls;
|
|
50
|
+
scheduler;
|
|
27
51
|
logger;
|
|
28
52
|
constructor(params) {
|
|
29
53
|
this.repo = params.repo;
|
|
@@ -31,14 +55,49 @@ var ContentService = class {
|
|
|
31
55
|
this.libraryAssets = params.libraryAssets;
|
|
32
56
|
this.files = params.files;
|
|
33
57
|
this.fileUrls = params.fileUrls;
|
|
58
|
+
this.scheduler = params.scheduler;
|
|
34
59
|
this.logger = params.logger ?? noopLogger;
|
|
35
60
|
}
|
|
61
|
+
require(actor, permission, on) {
|
|
62
|
+
this.requireAny(actor, [permission], on);
|
|
63
|
+
}
|
|
64
|
+
requireAny(actor, permissions, on) {
|
|
65
|
+
if (!permissions.some((permission) => actor.permissions.has(permission, on))) {
|
|
66
|
+
this.logger.warn("content action refused", {
|
|
67
|
+
orgId: actor.orgId,
|
|
68
|
+
actorId: actor.id,
|
|
69
|
+
permission: permissions.join(" or "),
|
|
70
|
+
contentId: on
|
|
71
|
+
});
|
|
72
|
+
throw new ForbiddenError(`You need ${permissions.join(" or ")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
visibleIds(actor, permissions, what) {
|
|
76
|
+
const wheres = permissions.map((permission) => actor.permissions.where(permission));
|
|
77
|
+
if (wheres.some((where) => where.org)) return;
|
|
78
|
+
const ids = [...new Set(wheres.flatMap((where) => where.content))];
|
|
79
|
+
if (ids.length === 0) {
|
|
80
|
+
this.logger.warn("content list refused", {
|
|
81
|
+
orgId: actor.orgId,
|
|
82
|
+
actorId: actor.id,
|
|
83
|
+
permission: permissions.join(" or ")
|
|
84
|
+
});
|
|
85
|
+
throw new ForbiddenError(`You cannot see ${what}`);
|
|
86
|
+
}
|
|
87
|
+
return ids;
|
|
88
|
+
}
|
|
36
89
|
withThumbnail(item) {
|
|
37
90
|
return {
|
|
38
91
|
...item,
|
|
39
|
-
thumbnailUrl: item.thumbnailAssetId ? this.fileUrls.
|
|
92
|
+
thumbnailUrl: !item.thumbnailAssetId ? null : shownPublicly(item) ? this.fileUrls.publicImage(item.orgId, item.id, item.thumbnailAssetId) : this.fileUrls.thumbnail(item.type, item.id)
|
|
40
93
|
};
|
|
41
94
|
}
|
|
95
|
+
syncThumbnail(item) {
|
|
96
|
+
return this.files.syncPublicImage(item.orgId, item.id, shownPublicly(item) ? item.thumbnailAssetId : null);
|
|
97
|
+
}
|
|
98
|
+
withdrawThumbnail(orgId, id) {
|
|
99
|
+
return this.files.syncPublicImage(orgId, id, null);
|
|
100
|
+
}
|
|
42
101
|
pageWithThumbnails(page) {
|
|
43
102
|
return {
|
|
44
103
|
...page,
|
|
@@ -56,14 +115,76 @@ var ContentService = class {
|
|
|
56
115
|
}] : [];
|
|
57
116
|
});
|
|
58
117
|
}
|
|
59
|
-
async
|
|
60
|
-
|
|
118
|
+
async eventFiles(orgId, links) {
|
|
119
|
+
const ready = await this.files.readyFiles(orgId, links.map((link) => link.assetId));
|
|
120
|
+
return links.flatMap((link) => {
|
|
121
|
+
const file = ready[link.assetId];
|
|
122
|
+
return file ? [{
|
|
123
|
+
...link,
|
|
124
|
+
...file,
|
|
125
|
+
url: this.fileUrls.eventFile(link.eventId, link.assetId)
|
|
126
|
+
}] : [];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async assertAttachable(actor, itemId, requested, attached, user) {
|
|
130
|
+
const ids = [...new Set(requested.filter((id) => typeof id === "string"))];
|
|
131
|
+
if (ids.length === 0) return;
|
|
132
|
+
const held = new Set(await attached());
|
|
133
|
+
const added = ids.filter((id) => !held.has(id));
|
|
134
|
+
if (added.length === 0) return;
|
|
135
|
+
const orgId = actor.orgId;
|
|
136
|
+
const found = new Map((await this.libraryAssets.libraryAssetsFor(orgId, added)).map((asset) => [asset.id, asset]));
|
|
137
|
+
const missing = added.filter((id) => !found.has(id));
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
this.logger.warn("library assets refused", {
|
|
140
|
+
orgId,
|
|
141
|
+
contentId: itemId,
|
|
142
|
+
assetIds: missing
|
|
143
|
+
});
|
|
144
|
+
throw new ConflictError(`${user} can only use ready files from the media library. Not available: ${missing.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
if (actor.permissions.has("media:read")) return;
|
|
147
|
+
const foreign = added.filter((id) => found.get(id)?.contentId !== itemId);
|
|
148
|
+
if (foreign.length > 0) {
|
|
149
|
+
this.logger.warn("library assets refused — not uploaded for this item", {
|
|
150
|
+
orgId,
|
|
151
|
+
actorId: actor.id,
|
|
152
|
+
contentId: itemId,
|
|
153
|
+
assetIds: foreign
|
|
154
|
+
});
|
|
155
|
+
throw new ForbiddenError(`You need media:read to use files that were not uploaded for this item: ${foreign.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async courseAssetIds(orgId, courseId) {
|
|
159
|
+
const [course, links] = await Promise.all([this.repo.findCourseById(orgId, courseId), this.repo.listActivityAssetsForCourse(orgId, courseId)]);
|
|
160
|
+
return [course?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
|
|
61
161
|
}
|
|
62
|
-
async
|
|
63
|
-
const
|
|
162
|
+
async downloadAssetIds(orgId, downloadId) {
|
|
163
|
+
const [download, links] = await Promise.all([this.repo.getDownload(orgId, downloadId), this.repo.listDownloadAssets(orgId, downloadId)]);
|
|
164
|
+
return [download?.thumbnailAssetId ?? null, ...links.map((link) => link.assetId)];
|
|
165
|
+
}
|
|
166
|
+
async eventAssetIds(orgId, eventId) {
|
|
167
|
+
const [event, links] = await Promise.all([this.repo.findEventById(orgId, eventId), this.repo.listEventAssets(orgId, eventId)]);
|
|
168
|
+
if (!event) throw new NotFoundError("Event", eventId);
|
|
169
|
+
return [event.thumbnailAssetId, ...links.map((link) => link.assetId)];
|
|
170
|
+
}
|
|
171
|
+
async listCourses(query, actor) {
|
|
172
|
+
const ids = this.visibleIds(actor, ["courses:read"], "courses");
|
|
173
|
+
return this.pageWithThumbnails(await this.repo.listCourses(actor.orgId, query, ids));
|
|
174
|
+
}
|
|
175
|
+
async getCourse(id, actor) {
|
|
176
|
+
const course = await this.findCourse(id, actor);
|
|
177
|
+
if (!course) throw new NotFoundError("Course", id);
|
|
178
|
+
return course;
|
|
179
|
+
}
|
|
180
|
+
async findCourse(id, actor) {
|
|
181
|
+
this.require(actor, "courses:read", id);
|
|
182
|
+
const course = await this.repo.findCourseById(actor.orgId, id);
|
|
64
183
|
return course ? this.withThumbnail(course) : null;
|
|
65
184
|
}
|
|
66
|
-
async createCourse(
|
|
185
|
+
async createCourse(input, actor) {
|
|
186
|
+
this.require(actor, "courses:write");
|
|
187
|
+
const orgId = actor.orgId;
|
|
67
188
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
68
189
|
const created = await content.createCourse(orgId, input, slugify(input.title));
|
|
69
190
|
await outbox.append([contentEvents.courseCreated.make({
|
|
@@ -75,12 +196,15 @@ var ContentService = class {
|
|
|
75
196
|
this.logger.info("course created", {
|
|
76
197
|
orgId,
|
|
77
198
|
courseId: course.id,
|
|
78
|
-
title: course.title
|
|
199
|
+
title: course.title,
|
|
200
|
+
actorId: actor.id
|
|
79
201
|
});
|
|
80
202
|
return this.withThumbnail(course);
|
|
81
203
|
}
|
|
82
|
-
async updateCourse(
|
|
83
|
-
|
|
204
|
+
async updateCourse(id, patch, actor) {
|
|
205
|
+
this.require(actor, "courses:write", id);
|
|
206
|
+
const orgId = actor.orgId;
|
|
207
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.courseAssetIds(orgId, id), "A course");
|
|
84
208
|
const course = await this.uow.run(async ({ content, outbox }) => {
|
|
85
209
|
const updated = await content.updateCourse(orgId, id, patch);
|
|
86
210
|
if (!updated) {
|
|
@@ -99,11 +223,15 @@ var ContentService = class {
|
|
|
99
223
|
this.logger.info("course updated", {
|
|
100
224
|
orgId,
|
|
101
225
|
courseId: id,
|
|
102
|
-
fields: Object.keys(patch)
|
|
226
|
+
fields: Object.keys(patch),
|
|
227
|
+
actorId: actor.id
|
|
103
228
|
});
|
|
229
|
+
await this.syncThumbnail(course);
|
|
104
230
|
return this.withThumbnail(course);
|
|
105
231
|
}
|
|
106
|
-
async patchCourseSettings(
|
|
232
|
+
async patchCourseSettings(id, value, actor) {
|
|
233
|
+
this.require(actor, "courses:write", id);
|
|
234
|
+
const orgId = actor.orgId;
|
|
107
235
|
const merged = await this.uow.run(async ({ content, outbox }) => {
|
|
108
236
|
const course = await content.findCourseById(orgId, id);
|
|
109
237
|
if (!course) {
|
|
@@ -126,11 +254,14 @@ var ContentService = class {
|
|
|
126
254
|
this.logger.info("course settings patched", {
|
|
127
255
|
orgId,
|
|
128
256
|
courseId: id,
|
|
129
|
-
value
|
|
257
|
+
value,
|
|
258
|
+
actorId: actor.id
|
|
130
259
|
});
|
|
131
260
|
return merged;
|
|
132
261
|
}
|
|
133
|
-
async deleteCourse(
|
|
262
|
+
async deleteCourse(id, actor) {
|
|
263
|
+
this.require(actor, "courses:delete", id);
|
|
264
|
+
const orgId = actor.orgId;
|
|
134
265
|
await this.uow.run(async ({ content, outbox }) => {
|
|
135
266
|
const course = await content.findCourseById(orgId, id);
|
|
136
267
|
if (!course) {
|
|
@@ -149,32 +280,44 @@ var ContentService = class {
|
|
|
149
280
|
});
|
|
150
281
|
this.logger.info("course deleted", {
|
|
151
282
|
orgId,
|
|
152
|
-
courseId: id
|
|
283
|
+
courseId: id,
|
|
284
|
+
actorId: actor.id
|
|
153
285
|
});
|
|
286
|
+
await this.withdrawThumbnail(orgId, id);
|
|
154
287
|
}
|
|
155
|
-
listCourseModules(
|
|
156
|
-
|
|
288
|
+
async listCourseModules(courseId, actor) {
|
|
289
|
+
this.require(actor, "courses:read", courseId);
|
|
290
|
+
return this.repo.listCourseModules(actor.orgId, courseId);
|
|
157
291
|
}
|
|
158
|
-
listCourseActivities(
|
|
159
|
-
|
|
292
|
+
async listCourseActivities(courseId, actor) {
|
|
293
|
+
this.require(actor, "courses:read", courseId);
|
|
294
|
+
return this.repo.listActivitiesForCourse(actor.orgId, courseId);
|
|
160
295
|
}
|
|
161
|
-
async listCourseActivityViews(
|
|
162
|
-
|
|
296
|
+
async listCourseActivityViews(courseId, actor) {
|
|
297
|
+
this.require(actor, "courses:read", courseId);
|
|
298
|
+
const [activities, links] = await Promise.all([this.repo.listActivitiesForCourse(actor.orgId, courseId), this.repo.listActivityAssetsForCourse(actor.orgId, courseId)]);
|
|
163
299
|
return activities.map((activity) => ({
|
|
164
300
|
...activity,
|
|
165
301
|
assetUrls: Object.fromEntries(links.filter((link) => link.activityId === activity.id).map((link) => [link.assetId, this.fileUrls.activityAsset(activity.id, link.assetId)]))
|
|
166
302
|
}));
|
|
167
303
|
}
|
|
168
|
-
listCourseActivityAssets(
|
|
169
|
-
|
|
304
|
+
async listCourseActivityAssets(courseId, actor) {
|
|
305
|
+
this.require(actor, "courses:read", courseId);
|
|
306
|
+
return this.repo.listActivityAssetsForCourse(actor.orgId, courseId);
|
|
170
307
|
}
|
|
171
|
-
getActivity(
|
|
172
|
-
|
|
308
|
+
async getActivity(activityId, actor) {
|
|
309
|
+
const activity = await this.repo.findActivity(actor.orgId, activityId);
|
|
310
|
+
if (activity) this.require(actor, "courses:read", activity.courseId);
|
|
311
|
+
return activity;
|
|
173
312
|
}
|
|
174
|
-
getModule(
|
|
175
|
-
|
|
313
|
+
async getModule(moduleId, actor) {
|
|
314
|
+
const module = await this.repo.findModule(actor.orgId, moduleId);
|
|
315
|
+
if (module) this.require(actor, "courses:read", module.courseId);
|
|
316
|
+
return module;
|
|
176
317
|
}
|
|
177
|
-
async reorderModules(
|
|
318
|
+
async reorderModules(courseId, orderedIds, actor) {
|
|
319
|
+
this.require(actor, "courses:write", courseId);
|
|
320
|
+
const orgId = actor.orgId;
|
|
178
321
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
179
322
|
const reordered = await content.reorderModules(orgId, courseId, orderedIds);
|
|
180
323
|
await outbox.append([contentEvents.modulesReordered.make({
|
|
@@ -190,7 +333,9 @@ var ContentService = class {
|
|
|
190
333
|
});
|
|
191
334
|
return modules;
|
|
192
335
|
}
|
|
193
|
-
async createModule(
|
|
336
|
+
async createModule(courseId, title, actor) {
|
|
337
|
+
this.require(actor, "courses:write", courseId);
|
|
338
|
+
const orgId = actor.orgId;
|
|
194
339
|
const { modules, created } = await this.uow.run(async ({ content, outbox }) => {
|
|
195
340
|
const all = await content.createModule(orgId, courseId, title);
|
|
196
341
|
const module = all.at(-1);
|
|
@@ -208,11 +353,14 @@ var ContentService = class {
|
|
|
208
353
|
orgId,
|
|
209
354
|
courseId,
|
|
210
355
|
moduleId: created.id,
|
|
211
|
-
title
|
|
356
|
+
title,
|
|
357
|
+
actorId: actor.id
|
|
212
358
|
});
|
|
213
359
|
return modules;
|
|
214
360
|
}
|
|
215
|
-
async updateModule(
|
|
361
|
+
async updateModule(courseId, moduleId, title, actor) {
|
|
362
|
+
this.require(actor, "courses:write", courseId);
|
|
363
|
+
const orgId = actor.orgId;
|
|
216
364
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
217
365
|
const existing = await content.findModule(orgId, moduleId);
|
|
218
366
|
if (!existing || existing.courseId !== courseId) {
|
|
@@ -236,11 +384,14 @@ var ContentService = class {
|
|
|
236
384
|
orgId,
|
|
237
385
|
courseId,
|
|
238
386
|
moduleId,
|
|
239
|
-
title
|
|
387
|
+
title,
|
|
388
|
+
actorId: actor.id
|
|
240
389
|
});
|
|
241
390
|
return modules;
|
|
242
391
|
}
|
|
243
|
-
async deleteModule(
|
|
392
|
+
async deleteModule(courseId, moduleId, actor) {
|
|
393
|
+
this.require(actor, "courses:write", courseId);
|
|
394
|
+
const orgId = actor.orgId;
|
|
244
395
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
245
396
|
const module = await content.findModule(orgId, moduleId);
|
|
246
397
|
if (!module || module.courseId !== courseId) {
|
|
@@ -261,11 +412,14 @@ var ContentService = class {
|
|
|
261
412
|
this.logger.info("module deleted", {
|
|
262
413
|
orgId,
|
|
263
414
|
courseId,
|
|
264
|
-
moduleId
|
|
415
|
+
moduleId,
|
|
416
|
+
actorId: actor.id
|
|
265
417
|
});
|
|
266
418
|
return modules;
|
|
267
419
|
}
|
|
268
|
-
async reorderActivities(
|
|
420
|
+
async reorderActivities(courseId, moduleId, orderedIds, actor) {
|
|
421
|
+
this.require(actor, "courses:write", courseId);
|
|
422
|
+
const orgId = actor.orgId;
|
|
269
423
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
270
424
|
const reordered = await content.reorderActivities(orgId, courseId, moduleId, orderedIds);
|
|
271
425
|
const module = reordered.find((m) => m.id === moduleId);
|
|
@@ -283,22 +437,12 @@ var ContentService = class {
|
|
|
283
437
|
});
|
|
284
438
|
return modules;
|
|
285
439
|
}
|
|
286
|
-
async
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
}
|
|
299
|
-
async saveActivity(orgId, courseId, moduleId, input, activityId) {
|
|
300
|
-
await this.assertLibraryAssets(orgId, input.assetIds ?? []);
|
|
440
|
+
async saveActivity(courseId, moduleId, input, activityId, actor) {
|
|
441
|
+
this.require(actor, "courses:write", courseId);
|
|
442
|
+
const orgId = actor.orgId;
|
|
443
|
+
await this.assertAttachable(actor, courseId, input.assetIds ?? [], () => this.courseAssetIds(orgId, courseId), "An activity");
|
|
301
444
|
const { modules, activity } = await this.uow.run(async ({ content, outbox }) => {
|
|
445
|
+
let wasPublished = false;
|
|
302
446
|
if (activityId) {
|
|
303
447
|
const existing = await content.findActivity(orgId, activityId);
|
|
304
448
|
if (!existing || existing.moduleId !== moduleId || existing.courseId !== courseId) {
|
|
@@ -310,6 +454,7 @@ var ContentService = class {
|
|
|
310
454
|
});
|
|
311
455
|
throw new NotFoundError("Activity", activityId);
|
|
312
456
|
}
|
|
457
|
+
wasPublished = isPublishedActivity(existing.settings);
|
|
313
458
|
} else {
|
|
314
459
|
const module = await content.findModule(orgId, moduleId);
|
|
315
460
|
if (!module || module.courseId !== courseId) {
|
|
@@ -321,14 +466,25 @@ var ContentService = class {
|
|
|
321
466
|
throw new NotFoundError("Module", moduleId);
|
|
322
467
|
}
|
|
323
468
|
}
|
|
324
|
-
const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId);
|
|
325
|
-
|
|
469
|
+
const { modules: all, activity: saved } = await content.saveActivity(orgId, courseId, moduleId, input, activityId ?? void 0);
|
|
470
|
+
const events = [activityId ? contentEvents.activityUpdated.make({
|
|
326
471
|
orgId,
|
|
327
472
|
data: saved
|
|
328
473
|
}) : contentEvents.activityCreated.make({
|
|
329
474
|
orgId,
|
|
330
475
|
data: saved
|
|
331
|
-
})]
|
|
476
|
+
})];
|
|
477
|
+
if (!wasPublished && isPublishedActivity(saved.settings)) {
|
|
478
|
+
if ((await content.findCourseById(orgId, courseId))?.status === "published") events.push(contentEvents.activityPublished.make({
|
|
479
|
+
orgId,
|
|
480
|
+
data: {
|
|
481
|
+
activityId: saved.id,
|
|
482
|
+
moduleId,
|
|
483
|
+
courseId
|
|
484
|
+
}
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
await outbox.append(events);
|
|
332
488
|
return {
|
|
333
489
|
modules: all,
|
|
334
490
|
activity: saved
|
|
@@ -338,11 +494,14 @@ var ContentService = class {
|
|
|
338
494
|
orgId,
|
|
339
495
|
courseId,
|
|
340
496
|
moduleId,
|
|
341
|
-
activityId: activity.id
|
|
497
|
+
activityId: activity.id,
|
|
498
|
+
actorId: actor.id
|
|
342
499
|
});
|
|
343
500
|
return modules;
|
|
344
501
|
}
|
|
345
|
-
async deleteActivity(
|
|
502
|
+
async deleteActivity(courseId, moduleId, activityId, actor) {
|
|
503
|
+
this.require(actor, "courses:write", courseId);
|
|
504
|
+
const orgId = actor.orgId;
|
|
346
505
|
const modules = await this.uow.run(async ({ content, outbox }) => {
|
|
347
506
|
const activity = await content.findActivity(orgId, activityId);
|
|
348
507
|
if (!activity || activity.moduleId !== moduleId || activity.courseId !== courseId) {
|
|
@@ -365,18 +524,28 @@ var ContentService = class {
|
|
|
365
524
|
orgId,
|
|
366
525
|
courseId,
|
|
367
526
|
moduleId,
|
|
368
|
-
activityId
|
|
527
|
+
activityId,
|
|
528
|
+
actorId: actor.id
|
|
369
529
|
});
|
|
370
530
|
return modules;
|
|
371
531
|
}
|
|
372
|
-
async listDownloads(
|
|
373
|
-
|
|
532
|
+
async listDownloads(query, actor) {
|
|
533
|
+
const ids = this.visibleIds(actor, ["downloads:read"], "downloads");
|
|
534
|
+
return this.pageWithThumbnails(await this.repo.listDownloads(actor.orgId, query, ids));
|
|
535
|
+
}
|
|
536
|
+
async getDownload(id, actor) {
|
|
537
|
+
const download = await this.findDownload(id, actor);
|
|
538
|
+
if (!download) throw new NotFoundError("Download", id);
|
|
539
|
+
return download;
|
|
374
540
|
}
|
|
375
|
-
async
|
|
376
|
-
|
|
541
|
+
async findDownload(id, actor) {
|
|
542
|
+
this.require(actor, "downloads:read", id);
|
|
543
|
+
const download = await this.repo.getDownload(actor.orgId, id);
|
|
377
544
|
return download ? this.withThumbnail(download) : null;
|
|
378
545
|
}
|
|
379
|
-
async createDownload(
|
|
546
|
+
async createDownload(input, actor) {
|
|
547
|
+
this.require(actor, "downloads:write");
|
|
548
|
+
const orgId = actor.orgId;
|
|
380
549
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
381
550
|
const created = await content.createDownload(orgId, input, slugify(input.title));
|
|
382
551
|
await outbox.append([contentEvents.downloadCreated.make({
|
|
@@ -388,12 +557,15 @@ var ContentService = class {
|
|
|
388
557
|
this.logger.info("download created", {
|
|
389
558
|
orgId,
|
|
390
559
|
downloadId: download.id,
|
|
391
|
-
title: download.title
|
|
560
|
+
title: download.title,
|
|
561
|
+
actorId: actor.id
|
|
392
562
|
});
|
|
393
563
|
return this.withThumbnail(download);
|
|
394
564
|
}
|
|
395
|
-
async updateDownload(
|
|
396
|
-
|
|
565
|
+
async updateDownload(id, patch, actor) {
|
|
566
|
+
this.require(actor, "downloads:write", id);
|
|
567
|
+
const orgId = actor.orgId;
|
|
568
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.downloadAssetIds(orgId, id), "A download");
|
|
397
569
|
const download = await this.uow.run(async ({ content, outbox }) => {
|
|
398
570
|
const updated = await content.updateDownload(orgId, id, patch);
|
|
399
571
|
if (!updated) {
|
|
@@ -412,11 +584,15 @@ var ContentService = class {
|
|
|
412
584
|
this.logger.info("download updated", {
|
|
413
585
|
orgId,
|
|
414
586
|
downloadId: id,
|
|
415
|
-
fields: Object.keys(patch)
|
|
587
|
+
fields: Object.keys(patch),
|
|
588
|
+
actorId: actor.id
|
|
416
589
|
});
|
|
590
|
+
await this.syncThumbnail(download);
|
|
417
591
|
return this.withThumbnail(download);
|
|
418
592
|
}
|
|
419
|
-
async removeDownload(
|
|
593
|
+
async removeDownload(id, actor) {
|
|
594
|
+
this.require(actor, "downloads:delete", id);
|
|
595
|
+
const orgId = actor.orgId;
|
|
420
596
|
await this.uow.run(async ({ content, outbox }) => {
|
|
421
597
|
const download = await content.getDownload(orgId, id);
|
|
422
598
|
if (!download) {
|
|
@@ -435,14 +611,19 @@ var ContentService = class {
|
|
|
435
611
|
});
|
|
436
612
|
this.logger.info("download deleted", {
|
|
437
613
|
orgId,
|
|
438
|
-
downloadId: id
|
|
614
|
+
downloadId: id,
|
|
615
|
+
actorId: actor.id
|
|
439
616
|
});
|
|
617
|
+
await this.withdrawThumbnail(orgId, id);
|
|
440
618
|
}
|
|
441
|
-
async listDownloadAssets(
|
|
442
|
-
|
|
619
|
+
async listDownloadAssets(downloadId, actor) {
|
|
620
|
+
this.require(actor, "downloads:read", downloadId);
|
|
621
|
+
return this.downloadFiles(actor.orgId, await this.repo.listDownloadAssets(actor.orgId, downloadId));
|
|
443
622
|
}
|
|
444
|
-
async addDownloadAsset(
|
|
445
|
-
|
|
623
|
+
async addDownloadAsset(downloadId, input, actor) {
|
|
624
|
+
this.require(actor, "downloads:write", downloadId);
|
|
625
|
+
const orgId = actor.orgId;
|
|
626
|
+
await this.assertAttachable(actor, downloadId, [input.assetId], () => this.downloadAssetIds(orgId, downloadId), "A download");
|
|
446
627
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
447
628
|
const assets = await content.addDownloadAsset(orgId, downloadId, input);
|
|
448
629
|
await outbox.append([contentEvents.downloadAssetAdded.make({
|
|
@@ -457,11 +638,14 @@ var ContentService = class {
|
|
|
457
638
|
this.logger.info("download asset added", {
|
|
458
639
|
orgId,
|
|
459
640
|
downloadId,
|
|
460
|
-
assetId: input.assetId
|
|
641
|
+
assetId: input.assetId,
|
|
642
|
+
actorId: actor.id
|
|
461
643
|
});
|
|
462
644
|
return this.downloadFiles(orgId, assets);
|
|
463
645
|
}
|
|
464
|
-
async removeDownloadAsset(
|
|
646
|
+
async removeDownloadAsset(downloadId, assetId, actor) {
|
|
647
|
+
this.require(actor, "downloads:write", downloadId);
|
|
648
|
+
const orgId = actor.orgId;
|
|
465
649
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
466
650
|
const assets = await content.removeDownloadAsset(orgId, downloadId, assetId);
|
|
467
651
|
await outbox.append([contentEvents.downloadAssetRemoved.make({
|
|
@@ -476,11 +660,14 @@ var ContentService = class {
|
|
|
476
660
|
this.logger.info("download asset removed", {
|
|
477
661
|
orgId,
|
|
478
662
|
downloadId,
|
|
479
|
-
assetId
|
|
663
|
+
assetId,
|
|
664
|
+
actorId: actor.id
|
|
480
665
|
});
|
|
481
666
|
return this.downloadFiles(orgId, assets);
|
|
482
667
|
}
|
|
483
|
-
async renameDownloadAsset(
|
|
668
|
+
async renameDownloadAsset(downloadId, assetId, displayName, actor) {
|
|
669
|
+
this.require(actor, "downloads:write", downloadId);
|
|
670
|
+
const orgId = actor.orgId;
|
|
484
671
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
485
672
|
const assets = await content.renameDownloadAsset(orgId, downloadId, assetId, displayName);
|
|
486
673
|
await outbox.append([contentEvents.downloadAssetRenamed.make({
|
|
@@ -495,13 +682,16 @@ var ContentService = class {
|
|
|
495
682
|
this.logger.info("download asset renamed", {
|
|
496
683
|
orgId,
|
|
497
684
|
downloadId,
|
|
498
|
-
assetId
|
|
685
|
+
assetId,
|
|
686
|
+
actorId: actor.id
|
|
499
687
|
});
|
|
500
688
|
return this.downloadFiles(orgId, assets);
|
|
501
689
|
}
|
|
502
690
|
/** The caller must send the COMPLETE ordered set — a partial list would
|
|
503
691
|
* silently unlink whatever it omitted. */
|
|
504
|
-
async reorderDownloadAssets(
|
|
692
|
+
async reorderDownloadAssets(downloadId, assetIds, actor) {
|
|
693
|
+
this.require(actor, "downloads:write", downloadId);
|
|
694
|
+
const orgId = actor.orgId;
|
|
505
695
|
const assets = await this.uow.run(async ({ content, outbox }) => {
|
|
506
696
|
const current = await content.listDownloadAssets(orgId, downloadId);
|
|
507
697
|
const currentIds = new Set(current.map((a) => a.assetId));
|
|
@@ -531,13 +721,330 @@ var ContentService = class {
|
|
|
531
721
|
});
|
|
532
722
|
return this.downloadFiles(orgId, assets);
|
|
533
723
|
}
|
|
534
|
-
|
|
535
|
-
|
|
724
|
+
async listEvents(query, actor) {
|
|
725
|
+
const ids = this.visibleIds(actor, ["events:read"], "events");
|
|
726
|
+
return this.pageWithThumbnails(await this.repo.listEvents(actor.orgId, query, ids));
|
|
727
|
+
}
|
|
728
|
+
async getEvent(id, actor) {
|
|
729
|
+
const event = await this.findEvent(id, actor);
|
|
730
|
+
if (!event) throw new NotFoundError("Event", id);
|
|
731
|
+
return event;
|
|
732
|
+
}
|
|
733
|
+
async findEvent(id, actor) {
|
|
734
|
+
this.require(actor, "events:read", id);
|
|
735
|
+
const event = await this.repo.findEventById(actor.orgId, id);
|
|
736
|
+
return event ? this.withThumbnail(event) : null;
|
|
737
|
+
}
|
|
738
|
+
async findEvents(ids, actor) {
|
|
739
|
+
for (const id of ids) this.require(actor, "events:read", id);
|
|
740
|
+
if (ids.length === 0) return [];
|
|
741
|
+
return (await this.repo.findEventsByIds(actor.orgId, ids)).map((event) => this.withThumbnail(event));
|
|
742
|
+
}
|
|
743
|
+
async createEvent(input, actor) {
|
|
744
|
+
this.require(actor, "events:write");
|
|
745
|
+
assertEventTimes(input.startsAt, input.endsAt);
|
|
746
|
+
const orgId = actor.orgId;
|
|
747
|
+
const { settings, ...fields } = input;
|
|
748
|
+
const event = await this.uow.run(async ({ content, outbox }) => {
|
|
749
|
+
const created = await content.createEvent(orgId, fields);
|
|
750
|
+
const event = settings ? {
|
|
751
|
+
...created,
|
|
752
|
+
settings: await content.patchEventSettings(orgId, created.id, settings)
|
|
753
|
+
} : created;
|
|
754
|
+
await outbox.append([contentEvents.eventCreated.make({
|
|
755
|
+
orgId,
|
|
756
|
+
data: event,
|
|
757
|
+
metadata: actorMetadata(actor)
|
|
758
|
+
})]);
|
|
759
|
+
return event;
|
|
760
|
+
});
|
|
761
|
+
this.logger.info("event created", {
|
|
762
|
+
orgId,
|
|
763
|
+
eventId: event.id,
|
|
764
|
+
title: event.title,
|
|
765
|
+
actorId: actor.id
|
|
766
|
+
});
|
|
767
|
+
return this.withThumbnail(event);
|
|
768
|
+
}
|
|
769
|
+
async updateEvent(id, patch, actor) {
|
|
770
|
+
this.require(actor, "events:write", id);
|
|
771
|
+
const orgId = actor.orgId;
|
|
772
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], () => this.eventAssetIds(orgId, id), "An event");
|
|
773
|
+
const { settings, ...fields } = patch;
|
|
774
|
+
const { previous, event } = await this.uow.run(async ({ content, outbox }) => {
|
|
775
|
+
const previous = await content.findEventById(orgId, id);
|
|
776
|
+
if (!previous) {
|
|
777
|
+
this.logger.warn("event update rejected — not found", {
|
|
778
|
+
orgId,
|
|
779
|
+
eventId: id
|
|
780
|
+
});
|
|
781
|
+
throw new NotFoundError("Event", id);
|
|
782
|
+
}
|
|
783
|
+
if (previous.status === "canceled") {
|
|
784
|
+
this.logger.warn("event update rejected — canceled", {
|
|
785
|
+
orgId,
|
|
786
|
+
eventId: id
|
|
787
|
+
});
|
|
788
|
+
throw new ConflictError("The event is canceled", "canceled");
|
|
789
|
+
}
|
|
790
|
+
assertEventTimes(fields.startsAt ?? previous.startsAt, fields.endsAt ?? previous.endsAt);
|
|
791
|
+
const updated = await content.updateEvent(orgId, id, fields);
|
|
792
|
+
if (!updated) throw new NotFoundError("Event", id);
|
|
793
|
+
const event = settings ? {
|
|
794
|
+
...updated,
|
|
795
|
+
settings: await content.patchEventSettings(orgId, id, settings)
|
|
796
|
+
} : updated;
|
|
797
|
+
await outbox.append([contentEvents.eventUpdated.make({
|
|
798
|
+
orgId,
|
|
799
|
+
data: event,
|
|
800
|
+
metadata: eventChange(actor, previous, event)
|
|
801
|
+
})]);
|
|
802
|
+
return {
|
|
803
|
+
previous,
|
|
804
|
+
event
|
|
805
|
+
};
|
|
806
|
+
});
|
|
807
|
+
this.logger.info("event updated", {
|
|
808
|
+
orgId,
|
|
809
|
+
eventId: id,
|
|
810
|
+
fields: Object.keys(patch),
|
|
811
|
+
actorId: actor.id
|
|
812
|
+
});
|
|
813
|
+
await this.scheduleStartingSoon(orgId, previous, event);
|
|
814
|
+
await this.syncThumbnail(event);
|
|
815
|
+
return this.withThumbnail(event);
|
|
816
|
+
}
|
|
817
|
+
async cancelEvent(id, actor) {
|
|
818
|
+
this.require(actor, "events:write", id);
|
|
819
|
+
const orgId = actor.orgId;
|
|
820
|
+
const event = await this.uow.run(async ({ content, outbox }) => {
|
|
821
|
+
const previous = await content.findEventById(orgId, id);
|
|
822
|
+
if (!previous) {
|
|
823
|
+
this.logger.warn("event cancel rejected — not found", {
|
|
824
|
+
orgId,
|
|
825
|
+
eventId: id
|
|
826
|
+
});
|
|
827
|
+
throw new NotFoundError("Event", id);
|
|
828
|
+
}
|
|
829
|
+
if (previous.status === "canceled") {
|
|
830
|
+
this.logger.warn("event cancel rejected — already canceled", {
|
|
831
|
+
orgId,
|
|
832
|
+
eventId: id
|
|
833
|
+
});
|
|
834
|
+
throw new ConflictError("The event is already canceled", "canceled");
|
|
835
|
+
}
|
|
836
|
+
const updated = await content.updateEvent(orgId, id, { status: "canceled" });
|
|
837
|
+
if (!updated) throw new NotFoundError("Event", id);
|
|
838
|
+
await outbox.append([contentEvents.eventUpdated.make({
|
|
839
|
+
orgId,
|
|
840
|
+
data: updated,
|
|
841
|
+
metadata: eventChange(actor, previous, updated)
|
|
842
|
+
})]);
|
|
843
|
+
return updated;
|
|
844
|
+
});
|
|
845
|
+
this.logger.info("event canceled", {
|
|
846
|
+
orgId,
|
|
847
|
+
eventId: id,
|
|
848
|
+
actorId: actor.id
|
|
849
|
+
});
|
|
850
|
+
await this.cancelStartingSoon(orgId, id);
|
|
851
|
+
await this.syncThumbnail(event);
|
|
852
|
+
return this.withThumbnail(event);
|
|
853
|
+
}
|
|
854
|
+
async deleteEvent(id, actor) {
|
|
855
|
+
this.require(actor, "events:delete", id);
|
|
856
|
+
const orgId = actor.orgId;
|
|
857
|
+
await this.uow.run(async ({ content, outbox }) => {
|
|
858
|
+
const event = await content.findEventById(orgId, id);
|
|
859
|
+
if (!event) {
|
|
860
|
+
this.logger.warn("event delete rejected — not found", {
|
|
861
|
+
orgId,
|
|
862
|
+
eventId: id
|
|
863
|
+
});
|
|
864
|
+
throw new NotFoundError("Event", id);
|
|
865
|
+
}
|
|
866
|
+
const linked = await content.spacesLinkingContent(orgId, id);
|
|
867
|
+
if (!await content.deleteEvent(orgId, id)) throw new NotFoundError("Event", id);
|
|
868
|
+
await content.deleteEventSettings(orgId, id);
|
|
869
|
+
await outbox.append([contentEvents.eventDeleted.make({
|
|
870
|
+
orgId,
|
|
871
|
+
data: event,
|
|
872
|
+
metadata: actorMetadata(actor)
|
|
873
|
+
}), ...await this.spaceUpdates(content, orgId, linked, actor)]);
|
|
874
|
+
});
|
|
875
|
+
this.logger.info("event deleted", {
|
|
876
|
+
orgId,
|
|
877
|
+
eventId: id,
|
|
878
|
+
actorId: actor.id
|
|
879
|
+
});
|
|
880
|
+
await this.withdrawThumbnail(orgId, id);
|
|
881
|
+
await this.cancelStartingSoon(orgId, id);
|
|
536
882
|
}
|
|
537
|
-
|
|
538
|
-
|
|
883
|
+
async startingSoon(orgId, eventId, lead, now) {
|
|
884
|
+
const event = await this.repo.findEventById(orgId, eventId);
|
|
885
|
+
if (!event || event.status !== "published") {
|
|
886
|
+
this.logger.debug("event no longer published", {
|
|
887
|
+
orgId,
|
|
888
|
+
eventId,
|
|
889
|
+
lead
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const due = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
|
|
894
|
+
if (due > now) {
|
|
895
|
+
await this.scheduler.at(EVENT_STARTING_SOON_JOB, due, startingSoonKey(eventId, lead), {
|
|
896
|
+
orgId,
|
|
897
|
+
eventId,
|
|
898
|
+
lead
|
|
899
|
+
});
|
|
900
|
+
this.logger.info("event starting soon timer moved", {
|
|
901
|
+
orgId,
|
|
902
|
+
eventId,
|
|
903
|
+
lead,
|
|
904
|
+
due: due.toISOString()
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
await this.uow.run(({ outbox }) => outbox.append([contentEvents.eventStartingSoon.make({
|
|
909
|
+
orgId,
|
|
910
|
+
data: {
|
|
911
|
+
eventId,
|
|
912
|
+
lead,
|
|
913
|
+
startsAt: event.startsAt
|
|
914
|
+
}
|
|
915
|
+
})]));
|
|
916
|
+
this.logger.info("event starting soon", {
|
|
917
|
+
orgId,
|
|
918
|
+
eventId,
|
|
919
|
+
lead
|
|
920
|
+
});
|
|
539
921
|
}
|
|
540
|
-
async
|
|
922
|
+
async scheduleStartingSoon(orgId, previous, event) {
|
|
923
|
+
if (event.status !== "published") {
|
|
924
|
+
if (previous.status === "published") await this.cancelStartingSoon(orgId, event.id);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (previous.status === "published" && previous.startsAt.getTime() === event.startsAt.getTime()) return;
|
|
928
|
+
const now = Date.now();
|
|
929
|
+
for (const lead of EVENT_LEAD_KEYS) {
|
|
930
|
+
const key = startingSoonKey(event.id, lead);
|
|
931
|
+
const runAt = new Date(event.startsAt.getTime() - EVENT_LEADS[lead]);
|
|
932
|
+
try {
|
|
933
|
+
if (runAt.getTime() > now) await this.scheduler.at(EVENT_STARTING_SOON_JOB, runAt, key, {
|
|
934
|
+
orgId,
|
|
935
|
+
eventId: event.id,
|
|
936
|
+
lead
|
|
937
|
+
});
|
|
938
|
+
else await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, key);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
this.logger.error("event starting soon timer not set", {
|
|
941
|
+
orgId,
|
|
942
|
+
eventId: event.id,
|
|
943
|
+
lead,
|
|
944
|
+
err
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async cancelStartingSoon(orgId, eventId) {
|
|
950
|
+
for (const lead of EVENT_LEAD_KEYS) try {
|
|
951
|
+
await this.scheduler.cancel(EVENT_STARTING_SOON_JOB, startingSoonKey(eventId, lead));
|
|
952
|
+
} catch (err) {
|
|
953
|
+
this.logger.warn("event starting soon timer not cancelled", {
|
|
954
|
+
orgId,
|
|
955
|
+
eventId,
|
|
956
|
+
lead,
|
|
957
|
+
err
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async listEventAssets(eventId, actor) {
|
|
962
|
+
this.require(actor, "events:read", eventId);
|
|
963
|
+
const orgId = actor.orgId;
|
|
964
|
+
if (!await this.repo.findEventById(orgId, eventId)) throw new NotFoundError("Event", eventId);
|
|
965
|
+
return this.eventFiles(orgId, await this.repo.listEventAssets(orgId, eventId));
|
|
966
|
+
}
|
|
967
|
+
async eventFilesFor(eventIds, actor) {
|
|
968
|
+
for (const id of eventIds) this.require(actor, "events:read", id);
|
|
969
|
+
const byEvent = Object.fromEntries(eventIds.map((id) => [id, []]));
|
|
970
|
+
if (eventIds.length === 0) return byEvent;
|
|
971
|
+
const files = await this.eventFiles(actor.orgId, await this.repo.listEventAssetsFor(actor.orgId, eventIds));
|
|
972
|
+
for (const file of files) byEvent[file.eventId]?.push(file);
|
|
973
|
+
return byEvent;
|
|
974
|
+
}
|
|
975
|
+
async addEventAsset(eventId, input, actor) {
|
|
976
|
+
this.require(actor, "events:write", eventId);
|
|
977
|
+
const orgId = actor.orgId;
|
|
978
|
+
await this.assertAttachable(actor, eventId, [input.assetId], () => this.eventAssetIds(orgId, eventId), "An event");
|
|
979
|
+
const assets = await this.uow.run(({ content }) => content.addEventAsset(orgId, eventId, input));
|
|
980
|
+
this.logger.info("event asset added", {
|
|
981
|
+
orgId,
|
|
982
|
+
eventId,
|
|
983
|
+
assetId: input.assetId,
|
|
984
|
+
actorId: actor.id
|
|
985
|
+
});
|
|
986
|
+
return this.eventFiles(orgId, assets);
|
|
987
|
+
}
|
|
988
|
+
async removeEventAsset(eventId, assetId, actor) {
|
|
989
|
+
this.require(actor, "events:write", eventId);
|
|
990
|
+
const orgId = actor.orgId;
|
|
991
|
+
const assets = await this.uow.run(({ content }) => content.removeEventAsset(orgId, eventId, assetId));
|
|
992
|
+
this.logger.info("event asset removed", {
|
|
993
|
+
orgId,
|
|
994
|
+
eventId,
|
|
995
|
+
assetId,
|
|
996
|
+
actorId: actor.id
|
|
997
|
+
});
|
|
998
|
+
return this.eventFiles(orgId, assets);
|
|
999
|
+
}
|
|
1000
|
+
async renameEventAsset(eventId, assetId, displayName, actor) {
|
|
1001
|
+
this.require(actor, "events:write", eventId);
|
|
1002
|
+
const orgId = actor.orgId;
|
|
1003
|
+
const assets = await this.uow.run(({ content }) => content.renameEventAsset(orgId, eventId, assetId, displayName));
|
|
1004
|
+
this.logger.info("event asset renamed", {
|
|
1005
|
+
orgId,
|
|
1006
|
+
eventId,
|
|
1007
|
+
assetId,
|
|
1008
|
+
actorId: actor.id
|
|
1009
|
+
});
|
|
1010
|
+
return this.eventFiles(orgId, assets);
|
|
1011
|
+
}
|
|
1012
|
+
async reorderEventAssets(eventId, assetIds, actor) {
|
|
1013
|
+
this.require(actor, "events:write", eventId);
|
|
1014
|
+
const orgId = actor.orgId;
|
|
1015
|
+
const assets = await this.uow.run(async ({ content }) => {
|
|
1016
|
+
if (!sameIdSet((await content.listEventAssets(orgId, eventId)).map((link) => link.assetId), assetIds)) {
|
|
1017
|
+
this.logger.warn("event asset reorder rejected — id set mismatch", {
|
|
1018
|
+
orgId,
|
|
1019
|
+
eventId,
|
|
1020
|
+
assetIds
|
|
1021
|
+
});
|
|
1022
|
+
throw new ConflictError("Ordered asset ids does not match the event's current assets");
|
|
1023
|
+
}
|
|
1024
|
+
return content.reorderEventAssets(orgId, eventId, assetIds);
|
|
1025
|
+
});
|
|
1026
|
+
this.logger.debug("event assets reordered", {
|
|
1027
|
+
orgId,
|
|
1028
|
+
eventId
|
|
1029
|
+
});
|
|
1030
|
+
return this.eventFiles(orgId, assets);
|
|
1031
|
+
}
|
|
1032
|
+
async listBundles(query, actor) {
|
|
1033
|
+
this.require(actor, "bundles:read");
|
|
1034
|
+
return this.repo.listBundles(actor.orgId, query);
|
|
1035
|
+
}
|
|
1036
|
+
async getBundle(id, actor) {
|
|
1037
|
+
const bundle = await this.findBundle(id, actor);
|
|
1038
|
+
if (!bundle) throw new NotFoundError("Bundle", id);
|
|
1039
|
+
return bundle;
|
|
1040
|
+
}
|
|
1041
|
+
async findBundle(id, actor) {
|
|
1042
|
+
this.require(actor, "bundles:read");
|
|
1043
|
+
return this.repo.findBundleById(actor.orgId, id);
|
|
1044
|
+
}
|
|
1045
|
+
async createBundle(input, actor) {
|
|
1046
|
+
this.require(actor, "bundles:write");
|
|
1047
|
+
const orgId = actor.orgId;
|
|
541
1048
|
const { bundle, items } = await this.uow.run(async ({ content, outbox }) => {
|
|
542
1049
|
const created = await content.createBundle(orgId, input);
|
|
543
1050
|
const initial = input.contentIds?.length ? await content.listBundleItems(orgId, created.id) : [];
|
|
@@ -560,11 +1067,14 @@ var ContentService = class {
|
|
|
560
1067
|
orgId,
|
|
561
1068
|
bundleId: bundle.id,
|
|
562
1069
|
name: bundle.name,
|
|
563
|
-
itemCount: items.length
|
|
1070
|
+
itemCount: items.length,
|
|
1071
|
+
actorId: actor.id
|
|
564
1072
|
});
|
|
565
1073
|
return bundle;
|
|
566
1074
|
}
|
|
567
|
-
async updateBundle(
|
|
1075
|
+
async updateBundle(id, patch, actor) {
|
|
1076
|
+
this.require(actor, "bundles:write");
|
|
1077
|
+
const orgId = actor.orgId;
|
|
568
1078
|
const bundle = await this.uow.run(async ({ content, outbox }) => {
|
|
569
1079
|
const updated = await content.updateBundle(orgId, id, patch);
|
|
570
1080
|
if (!updated) {
|
|
@@ -583,11 +1093,14 @@ var ContentService = class {
|
|
|
583
1093
|
this.logger.info("bundle updated", {
|
|
584
1094
|
orgId,
|
|
585
1095
|
bundleId: id,
|
|
586
|
-
fields: Object.keys(patch)
|
|
1096
|
+
fields: Object.keys(patch),
|
|
1097
|
+
actorId: actor.id
|
|
587
1098
|
});
|
|
588
1099
|
return bundle;
|
|
589
1100
|
}
|
|
590
|
-
async deleteBundle(
|
|
1101
|
+
async deleteBundle(id, actor) {
|
|
1102
|
+
this.require(actor, "bundles:delete");
|
|
1103
|
+
const orgId = actor.orgId;
|
|
591
1104
|
await this.uow.run(async ({ content, outbox }) => {
|
|
592
1105
|
const bundle = await content.findBundleById(orgId, id);
|
|
593
1106
|
if (!bundle) {
|
|
@@ -605,13 +1118,17 @@ var ContentService = class {
|
|
|
605
1118
|
});
|
|
606
1119
|
this.logger.info("bundle deleted", {
|
|
607
1120
|
orgId,
|
|
608
|
-
bundleId: id
|
|
1121
|
+
bundleId: id,
|
|
1122
|
+
actorId: actor.id
|
|
609
1123
|
});
|
|
610
1124
|
}
|
|
611
|
-
listBundleItems(
|
|
612
|
-
|
|
1125
|
+
async listBundleItems(bundleId, actor) {
|
|
1126
|
+
this.require(actor, "bundles:read");
|
|
1127
|
+
return this.repo.listBundleItems(actor.orgId, bundleId);
|
|
613
1128
|
}
|
|
614
|
-
async addBundleItem(
|
|
1129
|
+
async addBundleItem(bundleId, contentId, actor) {
|
|
1130
|
+
this.require(actor, "bundles:write");
|
|
1131
|
+
const orgId = actor.orgId;
|
|
615
1132
|
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
616
1133
|
const all = await content.addBundleItem(orgId, bundleId, contentId);
|
|
617
1134
|
await outbox.append([contentEvents.bundleItemAdded.make({
|
|
@@ -626,11 +1143,14 @@ var ContentService = class {
|
|
|
626
1143
|
this.logger.info("bundle item added", {
|
|
627
1144
|
orgId,
|
|
628
1145
|
bundleId,
|
|
629
|
-
contentId
|
|
1146
|
+
contentId,
|
|
1147
|
+
actorId: actor.id
|
|
630
1148
|
});
|
|
631
1149
|
return items;
|
|
632
1150
|
}
|
|
633
|
-
async removeBundleItem(
|
|
1151
|
+
async removeBundleItem(bundleId, contentId, actor) {
|
|
1152
|
+
this.require(actor, "bundles:write");
|
|
1153
|
+
const orgId = actor.orgId;
|
|
634
1154
|
const items = await this.uow.run(async ({ content, outbox }) => {
|
|
635
1155
|
const all = await content.removeBundleItem(orgId, bundleId, contentId);
|
|
636
1156
|
await outbox.append([contentEvents.bundleItemRemoved.make({
|
|
@@ -645,7 +1165,8 @@ var ContentService = class {
|
|
|
645
1165
|
this.logger.info("bundle item removed", {
|
|
646
1166
|
orgId,
|
|
647
1167
|
bundleId,
|
|
648
|
-
contentId
|
|
1168
|
+
contentId,
|
|
1169
|
+
actorId: actor.id
|
|
649
1170
|
});
|
|
650
1171
|
return items;
|
|
651
1172
|
}
|
|
@@ -678,35 +1199,58 @@ var ContentService = class {
|
|
|
678
1199
|
settings
|
|
679
1200
|
};
|
|
680
1201
|
}
|
|
681
|
-
async listCommunities(
|
|
682
|
-
|
|
1202
|
+
async listCommunities(query, actor) {
|
|
1203
|
+
const ids = this.visibleIds(actor, COMMUNITY_READERS, "communities");
|
|
1204
|
+
return this.pageWithThumbnails(await this.repo.listCommunities(actor.orgId, query, ids));
|
|
683
1205
|
}
|
|
684
|
-
async getCommunity(
|
|
685
|
-
const community = await this.
|
|
1206
|
+
async getCommunity(id, actor) {
|
|
1207
|
+
const community = await this.findCommunity(id, actor);
|
|
1208
|
+
if (!community) throw new NotFoundError("Community", id);
|
|
1209
|
+
return community;
|
|
1210
|
+
}
|
|
1211
|
+
async findCommunity(id, actor) {
|
|
1212
|
+
this.requireAny(actor, COMMUNITY_READERS, id);
|
|
1213
|
+
const community = await this.repo.findCommunityById(actor.orgId, id);
|
|
686
1214
|
return community ? this.withThumbnail(community) : null;
|
|
687
1215
|
}
|
|
688
|
-
|
|
689
|
-
return this.repo.
|
|
1216
|
+
communitiesWithDigest() {
|
|
1217
|
+
return this.repo.communitiesWithDigest();
|
|
690
1218
|
}
|
|
691
|
-
listCommunitiesById(
|
|
692
|
-
|
|
1219
|
+
async listCommunitiesById(ids, actor) {
|
|
1220
|
+
if (!actor.permissions.anywhere("communities:read")) {
|
|
1221
|
+
this.logger.warn("content list refused", {
|
|
1222
|
+
orgId: actor.orgId,
|
|
1223
|
+
actorId: actor.id,
|
|
1224
|
+
permission: "communities:read"
|
|
1225
|
+
});
|
|
1226
|
+
throw new ForbiddenError("You cannot see communities");
|
|
1227
|
+
}
|
|
1228
|
+
const visible = ids.filter((id) => actor.permissions.has("communities:read", id));
|
|
1229
|
+
return visible.length ? this.repo.findCommunitiesByIds(actor.orgId, visible) : [];
|
|
693
1230
|
}
|
|
694
|
-
getSpace(
|
|
695
|
-
|
|
1231
|
+
async getSpace(spaceId, actor) {
|
|
1232
|
+
const space = await this.repo.findSpace(actor.orgId, spaceId);
|
|
1233
|
+
if (space) this.require(actor, "communities:read", space.communityId);
|
|
1234
|
+
return space;
|
|
696
1235
|
}
|
|
697
|
-
async getCommunitySpace(
|
|
698
|
-
|
|
699
|
-
|
|
1236
|
+
async getCommunitySpace(communityId, spaceId, actor) {
|
|
1237
|
+
this.require(actor, "communities:read", communityId);
|
|
1238
|
+
const space = await this.repo.findSpace(actor.orgId, spaceId);
|
|
1239
|
+
if (space?.communityId !== communityId) throw new NotFoundError("Space", spaceId);
|
|
1240
|
+
return space;
|
|
700
1241
|
}
|
|
701
|
-
async listSpaces(
|
|
702
|
-
|
|
703
|
-
|
|
1242
|
+
async listSpaces(communityId, actor) {
|
|
1243
|
+
this.requireAny(actor, COMMUNITY_READERS, communityId);
|
|
1244
|
+
if (!await this.repo.findCommunityById(actor.orgId, communityId)) throw new NotFoundError("Community", communityId);
|
|
1245
|
+
return this.repo.listSpaces(actor.orgId, communityId);
|
|
704
1246
|
}
|
|
705
|
-
async getDmSettings(
|
|
706
|
-
|
|
1247
|
+
async getDmSettings(communityId, actor) {
|
|
1248
|
+
this.require(actor, "communities:read", communityId);
|
|
1249
|
+
return (await this.repo.findCommunityById(actor.orgId, communityId))?.settings.dm ?? null;
|
|
707
1250
|
}
|
|
708
|
-
async createCommunity(
|
|
709
|
-
|
|
1251
|
+
async createCommunity(input, actor) {
|
|
1252
|
+
this.require(actor, "communities:write");
|
|
1253
|
+
const orgId = actor.orgId;
|
|
710
1254
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
711
1255
|
const created = await content.createCommunity(orgId, input, slugify(input.title));
|
|
712
1256
|
await outbox.append([contentEvents.communityCreated.make({
|
|
@@ -723,9 +1267,10 @@ var ContentService = class {
|
|
|
723
1267
|
});
|
|
724
1268
|
return this.withThumbnail(community);
|
|
725
1269
|
}
|
|
726
|
-
async updateCommunity(
|
|
727
|
-
|
|
728
|
-
|
|
1270
|
+
async updateCommunity(id, patch, actor) {
|
|
1271
|
+
this.require(actor, "communities:write", id);
|
|
1272
|
+
const orgId = actor.orgId;
|
|
1273
|
+
await this.assertAttachable(actor, id, [patch.thumbnailAssetId], async () => [(await this.repo.findCommunityById(orgId, id))?.thumbnailAssetId ?? null], "A community");
|
|
729
1274
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
730
1275
|
const updated = await content.updateCommunity(orgId, id, patch);
|
|
731
1276
|
if (!updated) {
|
|
@@ -748,16 +1293,18 @@ var ContentService = class {
|
|
|
748
1293
|
communityId: id,
|
|
749
1294
|
actorId: actor.id
|
|
750
1295
|
});
|
|
1296
|
+
await this.syncThumbnail(community);
|
|
751
1297
|
return this.withThumbnail(community);
|
|
752
1298
|
}
|
|
753
|
-
publishCommunity(
|
|
754
|
-
return this.updateCommunity(
|
|
1299
|
+
publishCommunity(id, actor) {
|
|
1300
|
+
return this.updateCommunity(id, { status: "published" }, actor);
|
|
755
1301
|
}
|
|
756
|
-
unpublishCommunity(
|
|
757
|
-
return this.updateCommunity(
|
|
1302
|
+
unpublishCommunity(id, actor) {
|
|
1303
|
+
return this.updateCommunity(id, { status: "draft" }, actor);
|
|
758
1304
|
}
|
|
759
|
-
async deleteCommunity(
|
|
760
|
-
|
|
1305
|
+
async deleteCommunity(id, actor) {
|
|
1306
|
+
this.require(actor, "communities:delete", id);
|
|
1307
|
+
const orgId = actor.orgId;
|
|
761
1308
|
await this.uow.run(async ({ content, outbox }) => {
|
|
762
1309
|
const community = await content.findCommunityById(orgId, id);
|
|
763
1310
|
if (!community) {
|
|
@@ -782,13 +1329,15 @@ var ContentService = class {
|
|
|
782
1329
|
communityId: id,
|
|
783
1330
|
actorId: actor.id
|
|
784
1331
|
});
|
|
1332
|
+
await this.withdrawThumbnail(orgId, id);
|
|
785
1333
|
}
|
|
786
|
-
async
|
|
787
|
-
|
|
1334
|
+
async patchCommunitySettings(id, patch, actor) {
|
|
1335
|
+
this.require(actor, "communities:write", id);
|
|
1336
|
+
const orgId = actor.orgId;
|
|
788
1337
|
const community = await this.uow.run(async ({ content, outbox }) => {
|
|
789
|
-
const updated = await content.
|
|
1338
|
+
const updated = await content.patchCommunitySettings(orgId, id, patch);
|
|
790
1339
|
if (!updated) {
|
|
791
|
-
this.logger.warn("community
|
|
1340
|
+
this.logger.warn("community settings patch rejected — not found", {
|
|
792
1341
|
orgId,
|
|
793
1342
|
communityId: id,
|
|
794
1343
|
actorId: actor.id
|
|
@@ -802,15 +1351,16 @@ var ContentService = class {
|
|
|
802
1351
|
})]);
|
|
803
1352
|
return updated;
|
|
804
1353
|
});
|
|
805
|
-
this.logger.info("community
|
|
1354
|
+
this.logger.info("community settings patched", {
|
|
806
1355
|
orgId,
|
|
807
1356
|
communityId: id,
|
|
808
1357
|
actorId: actor.id
|
|
809
1358
|
});
|
|
810
1359
|
return this.withThumbnail(community);
|
|
811
1360
|
}
|
|
812
|
-
async createSpace(
|
|
813
|
-
|
|
1361
|
+
async createSpace(communityId, input, actor) {
|
|
1362
|
+
this.require(actor, "communities:write", communityId);
|
|
1363
|
+
const orgId = actor.orgId;
|
|
814
1364
|
const resolved = this.resolvedSpace(input);
|
|
815
1365
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
816
1366
|
if (!await content.findCommunityById(orgId, communityId)) {
|
|
@@ -842,8 +1392,9 @@ var ContentService = class {
|
|
|
842
1392
|
});
|
|
843
1393
|
return space;
|
|
844
1394
|
}
|
|
845
|
-
async updateSpace(
|
|
846
|
-
|
|
1395
|
+
async updateSpace(communityId, spaceId, patch, actor) {
|
|
1396
|
+
this.require(actor, "communities:write", communityId);
|
|
1397
|
+
const orgId = actor.orgId;
|
|
847
1398
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
848
1399
|
const current = await content.findSpace(orgId, spaceId);
|
|
849
1400
|
if (!current || current.communityId !== communityId) {
|
|
@@ -878,8 +1429,9 @@ var ContentService = class {
|
|
|
878
1429
|
});
|
|
879
1430
|
return space;
|
|
880
1431
|
}
|
|
881
|
-
async deleteSpace(
|
|
882
|
-
|
|
1432
|
+
async deleteSpace(communityId, spaceId, actor) {
|
|
1433
|
+
this.require(actor, "communities:write", communityId);
|
|
1434
|
+
const orgId = actor.orgId;
|
|
883
1435
|
await this.uow.run(async ({ content, outbox }) => {
|
|
884
1436
|
const space = await content.findSpace(orgId, spaceId);
|
|
885
1437
|
if (!space || space.communityId !== communityId) {
|
|
@@ -905,8 +1457,9 @@ var ContentService = class {
|
|
|
905
1457
|
actorId: actor.id
|
|
906
1458
|
});
|
|
907
1459
|
}
|
|
908
|
-
async reorderSpaces(
|
|
909
|
-
|
|
1460
|
+
async reorderSpaces(communityId, orderedIds, actor) {
|
|
1461
|
+
this.require(actor, "communities:write", communityId);
|
|
1462
|
+
const orgId = actor.orgId;
|
|
910
1463
|
const spaces = await this.uow.run(async ({ content, outbox }) => {
|
|
911
1464
|
if (!await content.findCommunityById(orgId, communityId)) {
|
|
912
1465
|
this.logger.warn("spaces reorder rejected — community not found", {
|
|
@@ -932,8 +1485,9 @@ var ContentService = class {
|
|
|
932
1485
|
});
|
|
933
1486
|
return spaces;
|
|
934
1487
|
}
|
|
935
|
-
async setSpaceTopics(
|
|
936
|
-
|
|
1488
|
+
async setSpaceTopics(communityId, spaceId, input, actor) {
|
|
1489
|
+
this.require(actor, "communities:write", communityId);
|
|
1490
|
+
const orgId = actor.orgId;
|
|
937
1491
|
const names = input.topics.map((topic) => topic.name);
|
|
938
1492
|
if (new Set(names).size !== names.length) throw new InvalidInputError("A space topic name is already used");
|
|
939
1493
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
@@ -968,8 +1522,9 @@ var ContentService = class {
|
|
|
968
1522
|
});
|
|
969
1523
|
return space;
|
|
970
1524
|
}
|
|
971
|
-
async addSpaceLink(
|
|
972
|
-
|
|
1525
|
+
async addSpaceLink(communityId, spaceId, contentId, actor) {
|
|
1526
|
+
this.require(actor, "communities:write", communityId);
|
|
1527
|
+
const orgId = actor.orgId;
|
|
973
1528
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
974
1529
|
const current = await content.findSpace(orgId, spaceId);
|
|
975
1530
|
if (!current || current.communityId !== communityId) {
|
|
@@ -999,8 +1554,9 @@ var ContentService = class {
|
|
|
999
1554
|
});
|
|
1000
1555
|
return space;
|
|
1001
1556
|
}
|
|
1002
|
-
async removeSpaceLink(
|
|
1003
|
-
|
|
1557
|
+
async removeSpaceLink(communityId, spaceId, linkId, actor) {
|
|
1558
|
+
this.require(actor, "communities:write", communityId);
|
|
1559
|
+
const orgId = actor.orgId;
|
|
1004
1560
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1005
1561
|
const current = await content.findSpace(orgId, spaceId);
|
|
1006
1562
|
if (!current || current.communityId !== communityId) {
|
|
@@ -1029,8 +1585,9 @@ var ContentService = class {
|
|
|
1029
1585
|
});
|
|
1030
1586
|
return space;
|
|
1031
1587
|
}
|
|
1032
|
-
async reorderSpaceLinks(
|
|
1033
|
-
|
|
1588
|
+
async reorderSpaceLinks(communityId, spaceId, orderedIds, actor) {
|
|
1589
|
+
this.require(actor, "communities:write", communityId);
|
|
1590
|
+
const orgId = actor.orgId;
|
|
1034
1591
|
const space = await this.uow.run(async ({ content, outbox }) => {
|
|
1035
1592
|
const current = await content.findSpace(orgId, spaceId);
|
|
1036
1593
|
if (!current || current.communityId !== communityId) {
|