@headless-lms/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +9 -9
- package/dist/assets/index.d.ts +3 -2
- package/dist/assets/index.js +2 -1
- 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 +4 -3
- package/dist/assets/service.d.ts +0 -1
- package/dist/assets/service.js +0 -4
- 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 +69 -62
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +4 -4
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +3 -17
- package/dist/automations/community-mail.js +1 -21
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +11 -4
- package/dist/automations/index.js +8 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +119 -5
- package/dist/automations/service.d.ts +10 -13
- package/dist/automations/service.js +21 -83
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +130 -80
- package/dist/content/events.js +11 -0
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +25 -6
- package/dist/content/service.d.ts +4 -4
- package/dist/content/service.js +24 -8
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/chats.d.ts +9 -4
- package/dist/discussion/chats.js +80 -47
- 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 +194 -28
- package/dist/discussion/events.js +74 -17
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -2
- package/dist/discussion/index.js +6 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +107 -24
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +18 -8
- package/dist/discussion/posts.js +207 -155
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.d.ts +17 -8
- package/dist/discussion/service.js +105 -41
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +2 -1
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/ports.d.ts +7 -3
- package/dist/entitlements/service.d.ts +5 -2
- package/dist/entitlements/service.js +23 -3
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +5 -4
- package/dist/identity/index.js +3 -2
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +78 -54
- package/dist/integrations/events.d.ts +3 -3
- package/dist/organizations/events.d.ts +32 -32
- package/dist/organizations/index.d.ts +2 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +3 -0
- package/dist/organizations/service.d.ts +2 -0
- package/dist/organizations/service.js +7 -0
- package/dist/organizations/service.js.map +1 -1
- package/dist/progress/events.d.ts +14 -14
- package/dist/reporting/member/index.d.ts +2 -2
- package/dist/reporting/member/model.d.ts +56 -23
- package/dist/reporting/member/ports.d.ts +9 -6
- package/dist/reporting/member/service.d.ts +33 -14
- package/dist/reporting/member/service.js +138 -60
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/errors.d.ts +13 -1
- package/dist/shared/errors.js +18 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +3 -0
- package/dist/shared/id.js +4 -1
- package/dist/shared/id.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 +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +2 -2
- package/dist/types/email-templates.d.ts +97 -3
- package/dist/types/identity.d.ts +2 -2
- package/dist/types/index.d.ts +7 -6
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- 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 +62 -1
- package/dist/types/schemas/content.js +25 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +85 -12
- package/dist/types/schemas/discussion.js +47 -13
- package/dist/types/schemas/discussion.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 +6 -5
- package/dist/types/schemas/index.js +6 -5
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/progress.d.ts +3 -3
- 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
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { isStaffRole } from "../../organizations/roles.js";
|
|
2
|
+
import { channelOrder } from "../../shared/notification-categories.js";
|
|
2
3
|
import { seesSpace } from "../../entitlements/rules.js";
|
|
4
|
+
import { resolveChannels } from "../../shared/notification-settings.js";
|
|
3
5
|
//#region src/reporting/member/service.ts
|
|
4
|
-
function ref
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
function refKey(ref) {
|
|
7
|
+
return `${ref.type}:${ref.id}`;
|
|
8
|
+
}
|
|
9
|
+
function itemView(ref, titles) {
|
|
10
|
+
const found = ref ? titles[refKey(ref)] : void 0;
|
|
11
|
+
if (!ref || !found) return null;
|
|
12
|
+
return found.excerpt !== void 0 ? {
|
|
13
|
+
type: ref.type,
|
|
14
|
+
id: ref.id,
|
|
15
|
+
excerpt: found.excerpt
|
|
16
|
+
} : {
|
|
17
|
+
type: ref.type,
|
|
18
|
+
id: ref.id,
|
|
19
|
+
title: found.title
|
|
20
|
+
};
|
|
10
21
|
}
|
|
11
22
|
function isActivityPublished(settings) {
|
|
12
23
|
return settings?.published !== false;
|
|
@@ -18,9 +29,12 @@ var MemberReportServiceImpl = class {
|
|
|
18
29
|
files;
|
|
19
30
|
fileUrls;
|
|
20
31
|
access;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
notificationList;
|
|
33
|
+
titles;
|
|
34
|
+
subscriptions;
|
|
35
|
+
notificationCategories;
|
|
36
|
+
notificationSettings;
|
|
37
|
+
channels;
|
|
24
38
|
constructor(params) {
|
|
25
39
|
this.repo = params.repo;
|
|
26
40
|
this.content = params.content;
|
|
@@ -28,72 +42,134 @@ var MemberReportServiceImpl = class {
|
|
|
28
42
|
this.files = params.files;
|
|
29
43
|
this.fileUrls = params.fileUrls;
|
|
30
44
|
this.access = params.access;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
45
|
+
this.notificationList = params.notifications;
|
|
46
|
+
this.titles = params.titles;
|
|
47
|
+
this.subscriptions = params.subscriptions;
|
|
48
|
+
this.notificationCategories = params.notificationCategories;
|
|
49
|
+
this.notificationSettings = params.notificationSettings;
|
|
50
|
+
this.channels = params.channels;
|
|
51
|
+
}
|
|
52
|
+
async communityNotificationSettings(orgId, actor, userId, communityId) {
|
|
53
|
+
if (!await this.access.isCommunityMember(orgId, actor, communityId)) return null;
|
|
54
|
+
const [list, settings, spaceIds, spaces, community] = await Promise.all([
|
|
55
|
+
this.notificationCategories.community.get(orgId, communityId),
|
|
56
|
+
this.notificationSettings.get(orgId, userId, { communityId }),
|
|
57
|
+
this.access.openSpaceIds(orgId, actor, communityId),
|
|
58
|
+
this.content.listSpaces(orgId, communityId),
|
|
59
|
+
this.content.getCommunity(orgId, communityId)
|
|
60
|
+
]);
|
|
61
|
+
const open = new Set(spaceIds);
|
|
62
|
+
return {
|
|
63
|
+
communityId,
|
|
64
|
+
digest: community?.settings.digest ?? null,
|
|
65
|
+
categories: this.settingsView(list.categories, settings, spaces.filter((space) => open.has(space.id)).map((space) => ({
|
|
66
|
+
spaceId: space.id,
|
|
67
|
+
name: space.name
|
|
68
|
+
})))
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async orgNotificationSettings(orgId, userId) {
|
|
72
|
+
const [list, settings] = await Promise.all([this.notificationCategories.org.get(orgId, orgId), this.notificationSettings.get(orgId, userId, "org")]);
|
|
73
|
+
return { categories: this.settingsView(list.categories, settings, []) };
|
|
74
|
+
}
|
|
75
|
+
settingsView(categories, settings, spaces) {
|
|
76
|
+
const configured = channelOrder(this.channels.list());
|
|
77
|
+
const channelsAt = (category, spaceId) => {
|
|
78
|
+
const withoutLevel = settings.filter((setting) => setting.category !== category.key || setting.spaceId !== spaceId);
|
|
79
|
+
const fallback = new Map(resolveChannels(category, withoutLevel, spaceId, configured).map((resolved) => [resolved.channel, resolved.enabled]));
|
|
80
|
+
return resolveChannels(category, settings, spaceId, configured).map((resolved) => ({
|
|
81
|
+
...resolved,
|
|
82
|
+
default: fallback.get(resolved.channel) ?? resolved.enabled
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
return categories.map((category) => {
|
|
86
|
+
const perSpace = category.perSpace === true;
|
|
87
|
+
const view = {
|
|
88
|
+
key: category.key,
|
|
89
|
+
perSpace,
|
|
90
|
+
channels: channelsAt(category, null)
|
|
91
|
+
};
|
|
92
|
+
if (perSpace) view.spaces = spaces.map(({ spaceId, name }) => ({
|
|
93
|
+
spaceId,
|
|
94
|
+
name,
|
|
95
|
+
channels: channelsAt(category, spaceId)
|
|
96
|
+
}));
|
|
97
|
+
return view;
|
|
98
|
+
});
|
|
34
99
|
}
|
|
35
|
-
async notifications(orgId, actor, userId,
|
|
36
|
-
const
|
|
100
|
+
async notifications(orgId, actor, userId, filter, page) {
|
|
101
|
+
const listed = await this.notificationList.list(orgId, userId, filter, page);
|
|
37
102
|
return {
|
|
38
|
-
rows: await this.describeNotifications(orgId, actor,
|
|
39
|
-
total:
|
|
103
|
+
rows: await this.describeNotifications(orgId, actor, listed.items),
|
|
104
|
+
total: listed.total,
|
|
40
105
|
page: page.page,
|
|
41
106
|
pageSize: page.pageSize,
|
|
42
|
-
unreadCount:
|
|
107
|
+
unreadCount: listed.unreadCount
|
|
43
108
|
};
|
|
44
109
|
}
|
|
45
|
-
async
|
|
46
|
-
const
|
|
47
|
-
return
|
|
110
|
+
async notification(orgId, actor, userId, notificationId) {
|
|
111
|
+
const found = await this.notificationList.get(orgId, userId, notificationId);
|
|
112
|
+
if (!found) return null;
|
|
113
|
+
const [view] = await this.describeNotifications(orgId, actor, [found]);
|
|
114
|
+
return view ?? null;
|
|
115
|
+
}
|
|
116
|
+
async notificationUnreadCounts(orgId, actor, userId) {
|
|
117
|
+
const communityIds = await this.access.openCommunityIds(orgId, actor);
|
|
118
|
+
return this.notificationList.unreadCounts(orgId, userId, communityIds);
|
|
48
119
|
}
|
|
49
120
|
async describeNotifications(orgId, actor, rows) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
121
|
+
const refs = rows.flatMap((row) => [
|
|
122
|
+
...row.subject ? [row.subject] : [],
|
|
123
|
+
...row.parent ? [row.parent] : [],
|
|
124
|
+
...row.communityId ? [{
|
|
125
|
+
type: "community",
|
|
126
|
+
id: row.communityId
|
|
127
|
+
}] : [],
|
|
128
|
+
...row.spaceId ? [{
|
|
129
|
+
type: "space",
|
|
130
|
+
id: row.spaceId
|
|
131
|
+
}] : []
|
|
132
|
+
]);
|
|
133
|
+
const actorIds = [...new Set(rows.flatMap((row) => row.actorOrgUserId ?? []))];
|
|
134
|
+
const [people, discussionTitles, contentTitles] = await Promise.all([
|
|
135
|
+
this.repo.people(orgId, actorIds),
|
|
136
|
+
this.titles.discussion.titlesFor(orgId, actor, refs),
|
|
137
|
+
this.titles.content.titlesFor(orgId, actor, refs)
|
|
57
138
|
]);
|
|
139
|
+
const titles = {
|
|
140
|
+
...contentTitles,
|
|
141
|
+
...discussionTitles
|
|
142
|
+
};
|
|
58
143
|
const personById = new Map(people.map((person) => [person.orgUserId, person]));
|
|
59
|
-
const spaceById = new Map(spaces.flatMap((space) => space ? [[space.id, space.name]] : []));
|
|
60
|
-
const communityById = new Map(communities.flatMap((community) => community ? [[community.id, community.title]] : []));
|
|
61
|
-
const postById = new Map(Object.entries(postTitles));
|
|
62
|
-
const chatById = new Map(Object.entries(chatTitles));
|
|
63
144
|
return rows.map((row) => {
|
|
64
145
|
const person = row.actorOrgUserId ? personById.get(row.actorOrgUserId) : void 0;
|
|
65
|
-
const community =
|
|
66
|
-
const space =
|
|
67
|
-
const post = ref(row.postId, postById);
|
|
68
|
-
const chat = ref(row.chatId, chatById);
|
|
146
|
+
const community = row.communityId ? titles[`community:${row.communityId}`] : void 0;
|
|
147
|
+
const space = row.spaceId ? titles[`space:${row.spaceId}`] : void 0;
|
|
69
148
|
return {
|
|
70
149
|
id: row.id,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
150
|
+
type: row.type,
|
|
151
|
+
scope: row.scope,
|
|
152
|
+
reasons: row.reasons,
|
|
153
|
+
matchedReason: row.matchedReason,
|
|
154
|
+
category: row.category,
|
|
155
|
+
display: row.display,
|
|
156
|
+
community: row.communityId && community ? {
|
|
157
|
+
id: row.communityId,
|
|
158
|
+
title: community.title
|
|
159
|
+
} : null,
|
|
160
|
+
space: row.spaceId && space ? {
|
|
161
|
+
id: row.spaceId,
|
|
162
|
+
name: space.title
|
|
163
|
+
} : null,
|
|
74
164
|
actor: person ? {
|
|
75
165
|
orgUserId: person.orgUserId,
|
|
76
|
-
|
|
166
|
+
name: person.displayName,
|
|
77
167
|
imageUrl: person.imageAssetId ? this.fileUrls.personImage(person.orgUserId) : null
|
|
78
168
|
} : null,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
space: space && {
|
|
84
|
-
id: space.id,
|
|
85
|
-
name: space.name
|
|
86
|
-
},
|
|
87
|
-
post: post && {
|
|
88
|
-
id: post.id,
|
|
89
|
-
title: post.name
|
|
90
|
-
},
|
|
91
|
-
commentId: row.commentId,
|
|
92
|
-
chat: chat && {
|
|
93
|
-
id: chat.id,
|
|
94
|
-
title: chat.name
|
|
95
|
-
},
|
|
96
|
-
messageId: row.messageId
|
|
169
|
+
subject: itemView(row.subject, titles),
|
|
170
|
+
parent: itemView(row.parent, titles),
|
|
171
|
+
createdAt: row.createdAt,
|
|
172
|
+
readAt: row.readAt
|
|
97
173
|
};
|
|
98
174
|
});
|
|
99
175
|
}
|
|
@@ -211,10 +287,11 @@ var MemberReportServiceImpl = class {
|
|
|
211
287
|
return view ?? null;
|
|
212
288
|
}
|
|
213
289
|
async homeSpaces(orgId, actor, communityId, spaces) {
|
|
214
|
-
const [openIds, counts, standings] = await Promise.all([
|
|
290
|
+
const [openIds, counts, standings, subscribed] = await Promise.all([
|
|
215
291
|
this.access.openSpaceIds(orgId, actor, communityId),
|
|
216
292
|
this.repo.spaceMemberCounts(orgId, communityId),
|
|
217
|
-
this.access.spaceStandings(orgId, actor, communityId)
|
|
293
|
+
this.access.spaceStandings(orgId, actor, communityId),
|
|
294
|
+
this.subscriptions.viewerSubscriptions(orgId, actor.id, "space", spaces.map((space) => space.id), /* @__PURE__ */ new Date())
|
|
218
295
|
]);
|
|
219
296
|
const open = new Set(openIds);
|
|
220
297
|
const seen = spaces.filter((space) => seesSpace(actor.role, space.accessRule, standings[space.id]));
|
|
@@ -241,7 +318,8 @@ var MemberReportServiceImpl = class {
|
|
|
241
318
|
id: topic.id,
|
|
242
319
|
name: topic.name
|
|
243
320
|
})),
|
|
244
|
-
links: await this.linkViews(orgId, actor, space.links)
|
|
321
|
+
links: await this.linkViews(orgId, actor, space.links),
|
|
322
|
+
subscription: subscribed[space.id] ?? null
|
|
245
323
|
};
|
|
246
324
|
}));
|
|
247
325
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/member/service.ts"],"sourcesContent":["import { type ContentServicePort, type SpaceDetail } from '../../content/index.js';\nimport { seesSpace, type EntitlementsService, type SpaceMember } from '../../entitlements/index.js';\nimport type { ProgressService } from '../../progress/index.js';\nimport type { AssetFiles, FileUrls } from '../../assets/index.js';\nimport { isStaffRole } from '../../organizations/index.js';\nimport type { ChatService, PostService } from '../../discussion/index.js';\nimport type { Notification, NotificationInbox } from '../../identity/index.js';\nimport type { Actor } from '../../shared/actor.js';\nimport type { Page } from '../../types/index.js';\nimport type {\n ActivityView,\n CommunityHome,\n CommunityHomeLink,\n CommunityHomeSpace,\n CourseProgressView,\n CourseView,\n DirectoryEntry,\n DirectoryQuery,\n DownloadFile,\n DownloadView,\n Module,\n NotificationView,\n NotificationsView,\n} from './model.js';\nimport type { MemberReportRepository, MemberReportService } from './ports.js';\n\nfunction ref(\n id: string | null,\n names: ReadonlyMap<string, string>,\n): { id: string; name: string } | null {\n const name = id ? names.get(id) : undefined;\n return id && name !== undefined ? { id, name } : null;\n}\n\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\ntype MemberAccess = Pick<\n EntitlementsService,\n 'isCommunityMember' | 'openSpaceIds' | 'spaceStandings' | 'hasAccess' | 'listAccessibleContent'\n>;\n\nexport type MemberReportServiceParams = {\n repo: MemberReportRepository;\n content: ContentServicePort;\n progress: ProgressService;\n files: Pick<AssetFiles, 'signFile'>;\n fileUrls: FileUrls;\n access: MemberAccess;\n notifications: Pick<NotificationInbox, 'listNotifications'>;\n posts: Pick<PostService, 'visibleTitles'>;\n chats: Pick<ChatService, 'titlesFor'>;\n};\n\nexport class MemberReportServiceImpl implements MemberReportService {\n private readonly repo: MemberReportRepository;\n private readonly content: ContentServicePort;\n private readonly progress: ProgressService;\n private readonly files: Pick<AssetFiles, 'signFile'>;\n private readonly fileUrls: FileUrls;\n private readonly access: MemberAccess;\n private readonly inbox: Pick<NotificationInbox, 'listNotifications'>;\n private readonly posts: Pick<PostService, 'visibleTitles'>;\n private readonly chats: Pick<ChatService, 'titlesFor'>;\n\n constructor(params: MemberReportServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.progress = params.progress;\n this.files = params.files;\n this.fileUrls = params.fileUrls;\n this.access = params.access;\n this.inbox = params.notifications;\n this.posts = params.posts;\n this.chats = params.chats;\n }\n\n async notifications(\n orgId: string,\n actor: Actor,\n userId: string,\n query: { page: number; pageSize: number },\n ): Promise<NotificationsView> {\n const page = await this.inbox.listNotifications(orgId, userId, query);\n return {\n rows: await this.describeNotifications(orgId, actor, page.rows),\n total: page.total,\n page: page.page,\n pageSize: page.pageSize,\n unreadCount: page.unreadCount,\n };\n }\n\n async notificationView(\n orgId: string,\n actor: Actor,\n notification: Notification,\n ): Promise<NotificationView> {\n const [view] = await this.describeNotifications(orgId, actor, [notification]);\n return view!;\n }\n\n private async describeNotifications(\n orgId: string,\n actor: Actor,\n rows: Notification[],\n ): Promise<NotificationView[]> {\n const ids = (pick: (row: Notification) => string | null): string[] => [\n ...new Set(rows.flatMap((row) => pick(row) ?? [])),\n ];\n const [people, postTitles, chatTitles, spaces, communities] = await Promise.all([\n this.repo.people(\n orgId,\n ids((row) => row.actorOrgUserId),\n ),\n this.posts.visibleTitles(\n orgId,\n actor,\n ids((row) => row.postId),\n ),\n this.chats.titlesFor(\n orgId,\n actor,\n ids((row) => row.chatId),\n ),\n Promise.all(ids((row) => row.spaceId).map((id) => this.content.getSpace(orgId, id))),\n Promise.all(ids((row) => row.communityId).map((id) => this.content.getCommunity(orgId, id))),\n ]);\n const personById = new Map(people.map((person) => [person.orgUserId, person]));\n const spaceById = new Map(spaces.flatMap((space) => (space ? [[space.id, space.name]] : [])));\n const communityById = new Map(\n communities.flatMap((community) => (community ? [[community.id, community.title]] : [])),\n );\n const postById = new Map(Object.entries(postTitles));\n const chatById = new Map(Object.entries(chatTitles));\n return rows.map((row) => {\n const person = row.actorOrgUserId ? personById.get(row.actorOrgUserId) : undefined;\n const community = ref(row.communityId, communityById);\n const space = ref(row.spaceId, spaceById);\n const post = ref(row.postId, postById);\n const chat = ref(row.chatId, chatById);\n return {\n id: row.id,\n kind: row.kind,\n createdAt: row.createdAt,\n readAt: row.readAt,\n actor: person\n ? {\n orgUserId: person.orgUserId,\n displayName: person.displayName,\n imageUrl: person.imageAssetId ? this.fileUrls.personImage(person.orgUserId) : null,\n }\n : null,\n community: community && { id: community.id, title: community.name },\n space: space && { id: space.id, name: space.name },\n post: post && { id: post.id, title: post.name },\n commentId: row.commentId,\n chat: chat && { id: chat.id, title: chat.name },\n messageId: row.messageId,\n };\n });\n }\n\n async listCourses(orgId: string, orgUserId: string): Promise<CourseView[]> {\n const ids = await this.access.listAccessibleContent(orgId, orgUserId, 'course');\n const courses = await Promise.all(ids.map((id) => this.content.getCourse(orgId, id)));\n return courses.filter((c): c is CourseView => c !== null && c.status === 'published');\n }\n\n async getCourse(orgId: string, orgUserId: string, courseId: string): Promise<CourseView | null> {\n if (!(await this.access.hasAccess(orgId, orgUserId, courseId))) {\n return null;\n }\n const course = await this.content.getCourse(orgId, courseId);\n return course && course.status === 'published' ? course : null;\n }\n\n async listModules(orgId: string, orgUserId: string, courseId: string): Promise<Module[] | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n return this.content.listCourseModules(orgId, courseId);\n }\n\n async listActivities(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<ActivityView[] | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n const activities = await this.content.listCourseActivityViews(orgId, courseId);\n return activities.filter((a) => isActivityPublished(a.settings));\n }\n\n async courseProgress(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<CourseProgressView | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n const courseActivities = await this.content.listCourseActivities(orgId, courseId);\n const ids = courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id);\n const records = await this.progress.listByTargets(orgId, orgUserId, ids);\n const activities: CourseProgressView['activities'] = {};\n const positions: CourseProgressView['positions'] = {};\n let done = 0;\n for (const r of records) {\n if (r.targetType !== 'activity') {\n continue;\n }\n activities[r.targetId] = r.completedAt ? 'completed' : 'in-progress';\n if (r.completedAt) {\n done += 1;\n }\n if (r.position != null) {\n positions[r.targetId] = r.position;\n }\n }\n const courseRecord = await this.progress.get(orgId, {\n orgUserId,\n targetType: 'course',\n targetId: courseId,\n });\n return {\n activities,\n percent: ids.length > 0 ? Math.round((done / ids.length) * 100) : 0,\n completed: courseRecord?.completedAt != null,\n positions,\n };\n }\n\n async listDownloads(orgId: string, orgUserId: string): Promise<DownloadView[]> {\n const ids = await this.access.listAccessibleContent(orgId, orgUserId, 'download');\n const rows = await Promise.all(ids.map((id) => this.content.getDownload(orgId, id)));\n return rows.filter((d): d is DownloadView => d !== null && d.status === 'published');\n }\n\n async getDownload(\n orgId: string,\n orgUserId: string,\n downloadId: string,\n ): Promise<{ download: DownloadView; assets: DownloadFile[] } | null> {\n if (!(await this.access.hasAccess(orgId, orgUserId, downloadId))) {\n return null;\n }\n const download = await this.content.getDownload(orgId, downloadId);\n if (!download || download.status !== 'published') {\n return null;\n }\n const assets = await this.content.listDownloadAssets(orgId, downloadId);\n return { download, assets };\n }\n\n async thumbnailFile(\n orgId: string,\n actor: Actor,\n owner: 'course' | 'download' | 'community',\n id: string,\n ): Promise<string | null> {\n const item =\n owner === 'course'\n ? await this.content.getCourse(orgId, id)\n : owner === 'download'\n ? await this.content.getDownload(orgId, id)\n : await this.content.getCommunity(orgId, id);\n if (!item?.thumbnailAssetId || (item.status !== 'published' && !isStaffRole(actor.role))) {\n return null;\n }\n return this.files.signFile(orgId, item.thumbnailAssetId);\n }\n\n async activityFile(\n orgId: string,\n actor: Actor,\n activityId: string,\n assetId: string,\n ): Promise<string | null> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n return null;\n }\n if (!isStaffRole(actor.role)) {\n if (\n !isActivityPublished(activity.settings) ||\n !(await this.getCourse(orgId, actor.id, activity.courseId))\n ) {\n return null;\n }\n }\n const links = await this.content.listCourseActivityAssets(orgId, activity.courseId);\n if (!links.some((link) => link.activityId === activityId && link.assetId === assetId)) {\n return null;\n }\n return this.files.signFile(orgId, assetId);\n }\n\n async downloadFile(\n orgId: string,\n actor: Actor,\n downloadId: string,\n assetId: string,\n ): Promise<string | null> {\n if (!isStaffRole(actor.role) && !(await this.getDownload(orgId, actor.id, downloadId))) {\n return null;\n }\n const files = await this.content.listDownloadAssets(orgId, downloadId);\n const file = files.find((f) => f.assetId === assetId);\n if (!file) {\n return null;\n }\n return this.files.signFile(orgId, assetId, { saveAs: file.displayName ?? file.filename });\n }\n\n async personImage(orgId: string, orgUserId: string): Promise<string | null> {\n const [person] = await this.repo.people(orgId, [orgUserId]);\n if (!person?.imageAssetId) {\n return null;\n }\n return this.files.signFile(orgId, person.imageAssetId);\n }\n\n async directory(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: DirectoryQuery,\n ): Promise<Page<DirectoryEntry> | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const community = await this.content.getCommunity(orgId, communityId);\n if (!community) {\n return null;\n }\n const page = await this.repo.directory(orgId, communityId, query);\n return {\n ...page,\n rows: page.rows.map((row) => ({\n ...row,\n imageUrl: row.imageAssetId ? this.fileUrls.personImage(row.orgUserId) : null,\n })),\n };\n }\n\n async communityHome(\n orgId: string,\n actor: Actor,\n communityId: string,\n ): Promise<CommunityHome | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const community = await this.content.getCommunity(orgId, communityId);\n if (!community) {\n return null;\n }\n const spaces = await this.content.listSpaces(orgId, communityId);\n return {\n communityId,\n title: community.title,\n description: community.description,\n thumbnailUrl: community.thumbnailUrl,\n dm: community.settings.dm,\n spaces: await this.homeSpaces(orgId, actor, communityId, spaces),\n };\n }\n\n async communitySpace(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<CommunityHomeSpace | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space || space.communityId !== communityId) {\n return null;\n }\n const [view] = await this.homeSpaces(orgId, actor, communityId, [space]);\n return view ?? null;\n }\n\n private async homeSpaces(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaces: SpaceDetail[],\n ): Promise<CommunityHomeSpace[]> {\n const [openIds, counts, standings] = await Promise.all([\n this.access.openSpaceIds(orgId, actor, communityId),\n this.repo.spaceMemberCounts(orgId, communityId),\n this.access.spaceStandings(orgId, actor, communityId),\n ]);\n const open = new Set(openIds);\n const seen = spaces.filter((space) =>\n seesSpace(actor.role, space.accessRule, standings[space.id]),\n );\n return Promise.all(\n seen.map(async (space): Promise<CommunityHomeSpace> => {\n const memberCount = counts[space.id] ?? 0;\n const base = {\n id: space.id,\n name: space.name,\n memberCount,\n accessRule: space.accessRule,\n standing: standingOf(space, open.has(space.id), memberCount, standings[space.id]),\n };\n if (!open.has(space.id)) {\n return { ...base, locked: true };\n }\n return {\n ...base,\n locked: false,\n description: space.description,\n capacity: space.capacity,\n settings: space.settings,\n topics: space.topics.map((topic) => ({ id: topic.id, name: topic.name })),\n links: await this.linkViews(orgId, actor, space.links),\n };\n }),\n );\n }\n\n private async linkViews(\n orgId: string,\n actor: Actor,\n links: SpaceDetail['links'],\n ): Promise<CommunityHomeLink[]> {\n const shown = (status: string) => status === 'published' || isStaffRole(actor.role);\n const views = await Promise.all(\n links.map(async (link): Promise<CommunityHomeLink | null> => {\n const course = await this.content.getCourse(orgId, link.contentId);\n if (course) {\n return shown(course.status)\n ? { id: link.id, contentId: link.contentId, type: 'course', title: course.title }\n : null;\n }\n const download = await this.content.getDownload(orgId, link.contentId);\n if (download) {\n return shown(download.status)\n ? { id: link.id, contentId: link.contentId, type: 'download', title: download.title }\n : null;\n }\n const community = await this.content.getCommunity(orgId, link.contentId);\n if (community) {\n return shown(community.status)\n ? { id: link.id, contentId: link.contentId, type: 'community', title: community.title }\n : null;\n }\n return null;\n }),\n );\n return views.filter((view): view is CommunityHomeLink => view !== null);\n }\n}\n\nfunction standingOf(\n space: SpaceDetail,\n open: boolean,\n memberCount: number,\n status: SpaceMember['status'] | undefined,\n): CommunityHomeSpace['standing'] {\n if (space.accessRule === 'all_members' || (open && status !== 'active')) {\n return 'open';\n }\n if (status === 'active') {\n return 'member';\n }\n if (status === 'requested' || status === 'invited') {\n return status;\n }\n if (space.capacity !== null && memberCount >= space.capacity) {\n return 'full';\n }\n return 'none';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/member/service.ts"],"sourcesContent":["import { type ContentServicePort, type SpaceDetail } from '../../content/index.js';\nimport { seesSpace, type EntitlementsService, type SpaceMember } from '../../entitlements/index.js';\nimport type { ProgressService } from '../../progress/index.js';\nimport type { AssetFiles, FileUrls } from '../../assets/index.js';\nimport { isStaffRole } from '../../organizations/index.js';\nimport type {\n ItemTitles as DiscussionItemTitles,\n SubscriptionService,\n} from '../../discussion/index.js';\nimport type { ItemTitles as ContentItemTitles } from '../../content/index.js';\nimport type { NotificationSettingsService } from '../../identity/index.js';\nimport type { ListedNotification, NotificationsService } from '../../automations/index.js';\nimport type { ChannelRegistry, NotificationCategoryService } from '../../shared/ports.js';\nimport { resolveChannels } from '../../shared/notification-settings.js';\nimport { channelOrder } from '../../shared/notification-categories.js';\nimport type {\n NotificationCategory,\n NotificationFilter,\n StoredSetting,\n SubjectRef,\n} from '../../types/index.js';\nimport type { Actor } from '../../shared/actor.js';\nimport type { Page } from '../../types/index.js';\nimport type {\n ActivityView,\n CommunityHome,\n CommunityHomeLink,\n CommunityHomeSpace,\n CourseProgressView,\n CourseView,\n DirectoryEntry,\n DirectoryQuery,\n DownloadFile,\n DownloadView,\n Module,\n NotificationItemView,\n NotificationView,\n NotificationsView,\n NotificationUnreadCounts,\n CommunityNotificationSettingsView,\n NotificationSettingsCategoryView,\n OrgNotificationSettingsView,\n SettingsChannelView,\n} from './model.js';\nimport type { MemberReportRepository, MemberReportService } from './ports.js';\n\nfunction refKey(ref: SubjectRef): string {\n return `${ref.type}:${ref.id}`;\n}\n\nfunction itemView(\n ref: SubjectRef | null,\n titles: Record<string, { title: string; excerpt?: string }>,\n): NotificationItemView | null {\n const found = ref ? titles[refKey(ref)] : undefined;\n if (!ref || !found) {\n return null;\n }\n return found.excerpt !== undefined\n ? { type: ref.type, id: ref.id, excerpt: found.excerpt }\n : { type: ref.type, id: ref.id, title: found.title };\n}\n\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\ntype MemberAccess = Pick<\n EntitlementsService,\n | 'isCommunityMember'\n | 'openSpaceIds'\n | 'spaceStandings'\n | 'hasAccess'\n | 'listAccessibleContent'\n | 'openCommunityIds'\n>;\n\nexport type MemberReportServiceParams = {\n repo: MemberReportRepository;\n content: ContentServicePort;\n progress: ProgressService;\n files: Pick<AssetFiles, 'signFile'>;\n fileUrls: FileUrls;\n access: MemberAccess;\n notifications: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n titles: { discussion: DiscussionItemTitles; content: ContentItemTitles };\n subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n notificationCategories: {\n community: Pick<NotificationCategoryService, 'get'>;\n org: Pick<NotificationCategoryService, 'get'>;\n };\n notificationSettings: Pick<NotificationSettingsService, 'get'>;\n channels: Pick<ChannelRegistry, 'list'>;\n};\n\nexport class MemberReportServiceImpl implements MemberReportService {\n private readonly repo: MemberReportRepository;\n private readonly content: ContentServicePort;\n private readonly progress: ProgressService;\n private readonly files: Pick<AssetFiles, 'signFile'>;\n private readonly fileUrls: FileUrls;\n private readonly access: MemberAccess;\n private readonly notificationList: Pick<NotificationsService, 'list' | 'get' | 'unreadCounts'>;\n private readonly titles: MemberReportServiceParams['titles'];\n private readonly subscriptions: Pick<SubscriptionService, 'viewerSubscriptions'>;\n private readonly notificationCategories: MemberReportServiceParams['notificationCategories'];\n private readonly notificationSettings: Pick<NotificationSettingsService, 'get'>;\n private readonly channels: Pick<ChannelRegistry, 'list'>;\n\n constructor(params: MemberReportServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.progress = params.progress;\n this.files = params.files;\n this.fileUrls = params.fileUrls;\n this.access = params.access;\n this.notificationList = params.notifications;\n this.titles = params.titles;\n this.subscriptions = params.subscriptions;\n this.notificationCategories = params.notificationCategories;\n this.notificationSettings = params.notificationSettings;\n this.channels = params.channels;\n }\n\n async communityNotificationSettings(\n orgId: string,\n actor: Actor,\n userId: string,\n communityId: string,\n ): Promise<CommunityNotificationSettingsView | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const [list, settings, spaceIds, spaces, community] = await Promise.all([\n this.notificationCategories.community.get(orgId, communityId),\n this.notificationSettings.get(orgId, userId, { communityId }),\n this.access.openSpaceIds(orgId, actor, communityId),\n this.content.listSpaces(orgId, communityId),\n this.content.getCommunity(orgId, communityId),\n ]);\n const open = new Set(spaceIds);\n return {\n communityId,\n digest: community?.settings.digest ?? null,\n categories: this.settingsView(\n list.categories,\n settings,\n spaces\n .filter((space) => open.has(space.id))\n .map((space) => ({ spaceId: space.id, name: space.name })),\n ),\n };\n }\n\n async orgNotificationSettings(\n orgId: string,\n userId: string,\n ): Promise<OrgNotificationSettingsView> {\n const [list, settings] = await Promise.all([\n this.notificationCategories.org.get(orgId, orgId),\n this.notificationSettings.get(orgId, userId, 'org'),\n ]);\n return { categories: this.settingsView(list.categories, settings, []) };\n }\n\n private settingsView(\n categories: NotificationCategory[],\n settings: StoredSetting[],\n spaces: { spaceId: string; name: string }[],\n ): NotificationSettingsCategoryView[] {\n const configured = channelOrder(this.channels.list());\n const channelsAt = (\n category: NotificationCategory,\n spaceId: string | null,\n ): SettingsChannelView[] => {\n const withoutLevel = settings.filter(\n (setting) => setting.category !== category.key || setting.spaceId !== spaceId,\n );\n const fallback = new Map(\n resolveChannels(category, withoutLevel, spaceId, configured).map((resolved) => [\n resolved.channel,\n resolved.enabled,\n ]),\n );\n return resolveChannels(category, settings, spaceId, configured).map((resolved) => ({\n ...resolved,\n default: fallback.get(resolved.channel) ?? resolved.enabled,\n }));\n };\n return categories.map((category) => {\n const perSpace = category.perSpace === true;\n const view: NotificationSettingsCategoryView = {\n key: category.key,\n perSpace,\n channels: channelsAt(category, null),\n };\n if (perSpace) {\n view.spaces = spaces.map(({ spaceId, name }) => ({\n spaceId,\n name,\n channels: channelsAt(category, spaceId),\n }));\n }\n return view;\n });\n }\n\n async notifications(\n orgId: string,\n actor: Actor,\n userId: string,\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n ): Promise<NotificationsView> {\n const listed = await this.notificationList.list(orgId, userId, filter, page);\n return {\n rows: await this.describeNotifications(orgId, actor, listed.items),\n total: listed.total,\n page: page.page,\n pageSize: page.pageSize,\n unreadCount: listed.unreadCount,\n };\n }\n\n async notification(\n orgId: string,\n actor: Actor,\n userId: string,\n notificationId: string,\n ): Promise<NotificationView | null> {\n const found = await this.notificationList.get(orgId, userId, notificationId);\n if (!found) {\n return null;\n }\n const [view] = await this.describeNotifications(orgId, actor, [found]);\n return view ?? null;\n }\n\n async notificationUnreadCounts(\n orgId: string,\n actor: Actor,\n userId: string,\n ): Promise<NotificationUnreadCounts> {\n const communityIds = await this.access.openCommunityIds(orgId, actor);\n return this.notificationList.unreadCounts(orgId, userId, communityIds);\n }\n\n private async describeNotifications(\n orgId: string,\n actor: Actor,\n rows: ListedNotification[],\n ): Promise<NotificationView[]> {\n const refs = rows.flatMap((row) => [\n ...(row.subject ? [row.subject] : []),\n ...(row.parent ? [row.parent] : []),\n ...(row.communityId ? [{ type: 'community' as const, id: row.communityId }] : []),\n ...(row.spaceId ? [{ type: 'space' as const, id: row.spaceId }] : []),\n ]);\n const actorIds = [...new Set(rows.flatMap((row) => row.actorOrgUserId ?? []))];\n const [people, discussionTitles, contentTitles] = await Promise.all([\n this.repo.people(orgId, actorIds),\n this.titles.discussion.titlesFor(orgId, actor, refs),\n this.titles.content.titlesFor(orgId, actor, refs),\n ]);\n const titles = { ...contentTitles, ...discussionTitles };\n const personById = new Map(people.map((person) => [person.orgUserId, person]));\n return rows.map((row) => {\n const person = row.actorOrgUserId ? personById.get(row.actorOrgUserId) : undefined;\n const community = row.communityId ? titles[`community:${row.communityId}`] : undefined;\n const space = row.spaceId ? titles[`space:${row.spaceId}`] : undefined;\n return {\n id: row.id,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n display: row.display,\n community:\n row.communityId && community ? { id: row.communityId, title: community.title } : null,\n space: row.spaceId && space ? { id: row.spaceId, name: space.title } : null,\n actor: person\n ? {\n orgUserId: person.orgUserId,\n name: person.displayName,\n imageUrl: person.imageAssetId ? this.fileUrls.personImage(person.orgUserId) : null,\n }\n : null,\n subject: itemView(row.subject, titles),\n parent: itemView(row.parent, titles),\n createdAt: row.createdAt,\n readAt: row.readAt,\n };\n });\n }\n\n async listCourses(orgId: string, orgUserId: string): Promise<CourseView[]> {\n const ids = await this.access.listAccessibleContent(orgId, orgUserId, 'course');\n const courses = await Promise.all(ids.map((id) => this.content.getCourse(orgId, id)));\n return courses.filter((c): c is CourseView => c !== null && c.status === 'published');\n }\n\n async getCourse(orgId: string, orgUserId: string, courseId: string): Promise<CourseView | null> {\n if (!(await this.access.hasAccess(orgId, orgUserId, courseId))) {\n return null;\n }\n const course = await this.content.getCourse(orgId, courseId);\n return course && course.status === 'published' ? course : null;\n }\n\n async listModules(orgId: string, orgUserId: string, courseId: string): Promise<Module[] | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n return this.content.listCourseModules(orgId, courseId);\n }\n\n async listActivities(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<ActivityView[] | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n const activities = await this.content.listCourseActivityViews(orgId, courseId);\n return activities.filter((a) => isActivityPublished(a.settings));\n }\n\n async courseProgress(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<CourseProgressView | null> {\n if (!(await this.getCourse(orgId, orgUserId, courseId))) {\n return null;\n }\n const courseActivities = await this.content.listCourseActivities(orgId, courseId);\n const ids = courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id);\n const records = await this.progress.listByTargets(orgId, orgUserId, ids);\n const activities: CourseProgressView['activities'] = {};\n const positions: CourseProgressView['positions'] = {};\n let done = 0;\n for (const r of records) {\n if (r.targetType !== 'activity') {\n continue;\n }\n activities[r.targetId] = r.completedAt ? 'completed' : 'in-progress';\n if (r.completedAt) {\n done += 1;\n }\n if (r.position != null) {\n positions[r.targetId] = r.position;\n }\n }\n const courseRecord = await this.progress.get(orgId, {\n orgUserId,\n targetType: 'course',\n targetId: courseId,\n });\n return {\n activities,\n percent: ids.length > 0 ? Math.round((done / ids.length) * 100) : 0,\n completed: courseRecord?.completedAt != null,\n positions,\n };\n }\n\n async listDownloads(orgId: string, orgUserId: string): Promise<DownloadView[]> {\n const ids = await this.access.listAccessibleContent(orgId, orgUserId, 'download');\n const rows = await Promise.all(ids.map((id) => this.content.getDownload(orgId, id)));\n return rows.filter((d): d is DownloadView => d !== null && d.status === 'published');\n }\n\n async getDownload(\n orgId: string,\n orgUserId: string,\n downloadId: string,\n ): Promise<{ download: DownloadView; assets: DownloadFile[] } | null> {\n if (!(await this.access.hasAccess(orgId, orgUserId, downloadId))) {\n return null;\n }\n const download = await this.content.getDownload(orgId, downloadId);\n if (!download || download.status !== 'published') {\n return null;\n }\n const assets = await this.content.listDownloadAssets(orgId, downloadId);\n return { download, assets };\n }\n\n async thumbnailFile(\n orgId: string,\n actor: Actor,\n owner: 'course' | 'download' | 'community',\n id: string,\n ): Promise<string | null> {\n const item =\n owner === 'course'\n ? await this.content.getCourse(orgId, id)\n : owner === 'download'\n ? await this.content.getDownload(orgId, id)\n : await this.content.getCommunity(orgId, id);\n if (!item?.thumbnailAssetId || (item.status !== 'published' && !isStaffRole(actor.role))) {\n return null;\n }\n return this.files.signFile(orgId, item.thumbnailAssetId);\n }\n\n async activityFile(\n orgId: string,\n actor: Actor,\n activityId: string,\n assetId: string,\n ): Promise<string | null> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n return null;\n }\n if (!isStaffRole(actor.role)) {\n if (\n !isActivityPublished(activity.settings) ||\n !(await this.getCourse(orgId, actor.id, activity.courseId))\n ) {\n return null;\n }\n }\n const links = await this.content.listCourseActivityAssets(orgId, activity.courseId);\n if (!links.some((link) => link.activityId === activityId && link.assetId === assetId)) {\n return null;\n }\n return this.files.signFile(orgId, assetId);\n }\n\n async downloadFile(\n orgId: string,\n actor: Actor,\n downloadId: string,\n assetId: string,\n ): Promise<string | null> {\n if (!isStaffRole(actor.role) && !(await this.getDownload(orgId, actor.id, downloadId))) {\n return null;\n }\n const files = await this.content.listDownloadAssets(orgId, downloadId);\n const file = files.find((f) => f.assetId === assetId);\n if (!file) {\n return null;\n }\n return this.files.signFile(orgId, assetId, { saveAs: file.displayName ?? file.filename });\n }\n\n async personImage(orgId: string, orgUserId: string): Promise<string | null> {\n const [person] = await this.repo.people(orgId, [orgUserId]);\n if (!person?.imageAssetId) {\n return null;\n }\n return this.files.signFile(orgId, person.imageAssetId);\n }\n\n async directory(\n orgId: string,\n actor: Actor,\n communityId: string,\n query: DirectoryQuery,\n ): Promise<Page<DirectoryEntry> | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const community = await this.content.getCommunity(orgId, communityId);\n if (!community) {\n return null;\n }\n const page = await this.repo.directory(orgId, communityId, query);\n return {\n ...page,\n rows: page.rows.map((row) => ({\n ...row,\n imageUrl: row.imageAssetId ? this.fileUrls.personImage(row.orgUserId) : null,\n })),\n };\n }\n\n async communityHome(\n orgId: string,\n actor: Actor,\n communityId: string,\n ): Promise<CommunityHome | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const community = await this.content.getCommunity(orgId, communityId);\n if (!community) {\n return null;\n }\n const spaces = await this.content.listSpaces(orgId, communityId);\n return {\n communityId,\n title: community.title,\n description: community.description,\n thumbnailUrl: community.thumbnailUrl,\n dm: community.settings.dm,\n spaces: await this.homeSpaces(orgId, actor, communityId, spaces),\n };\n }\n\n async communitySpace(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<CommunityHomeSpace | null> {\n if (!(await this.access.isCommunityMember(orgId, actor, communityId))) {\n return null;\n }\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space || space.communityId !== communityId) {\n return null;\n }\n const [view] = await this.homeSpaces(orgId, actor, communityId, [space]);\n return view ?? null;\n }\n\n private async homeSpaces(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaces: SpaceDetail[],\n ): Promise<CommunityHomeSpace[]> {\n const [openIds, counts, standings, subscribed] = await Promise.all([\n this.access.openSpaceIds(orgId, actor, communityId),\n this.repo.spaceMemberCounts(orgId, communityId),\n this.access.spaceStandings(orgId, actor, communityId),\n this.subscriptions.viewerSubscriptions(\n orgId,\n actor.id,\n 'space',\n spaces.map((space) => space.id),\n new Date(),\n ),\n ]);\n const open = new Set(openIds);\n const seen = spaces.filter((space) =>\n seesSpace(actor.role, space.accessRule, standings[space.id]),\n );\n return Promise.all(\n seen.map(async (space): Promise<CommunityHomeSpace> => {\n const memberCount = counts[space.id] ?? 0;\n const base = {\n id: space.id,\n name: space.name,\n memberCount,\n accessRule: space.accessRule,\n standing: standingOf(space, open.has(space.id), memberCount, standings[space.id]),\n };\n if (!open.has(space.id)) {\n return { ...base, locked: true };\n }\n return {\n ...base,\n locked: false,\n description: space.description,\n capacity: space.capacity,\n settings: space.settings,\n topics: space.topics.map((topic) => ({ id: topic.id, name: topic.name })),\n links: await this.linkViews(orgId, actor, space.links),\n subscription: subscribed[space.id] ?? null,\n };\n }),\n );\n }\n\n private async linkViews(\n orgId: string,\n actor: Actor,\n links: SpaceDetail['links'],\n ): Promise<CommunityHomeLink[]> {\n const shown = (status: string) => status === 'published' || isStaffRole(actor.role);\n const views = await Promise.all(\n links.map(async (link): Promise<CommunityHomeLink | null> => {\n const course = await this.content.getCourse(orgId, link.contentId);\n if (course) {\n return shown(course.status)\n ? { id: link.id, contentId: link.contentId, type: 'course', title: course.title }\n : null;\n }\n const download = await this.content.getDownload(orgId, link.contentId);\n if (download) {\n return shown(download.status)\n ? { id: link.id, contentId: link.contentId, type: 'download', title: download.title }\n : null;\n }\n const community = await this.content.getCommunity(orgId, link.contentId);\n if (community) {\n return shown(community.status)\n ? { id: link.id, contentId: link.contentId, type: 'community', title: community.title }\n : null;\n }\n return null;\n }),\n );\n return views.filter((view): view is CommunityHomeLink => view !== null);\n }\n}\n\nfunction standingOf(\n space: SpaceDetail,\n open: boolean,\n memberCount: number,\n status: SpaceMember['status'] | undefined,\n): CommunityHomeSpace['standing'] {\n if (space.accessRule === 'all_members' || (open && status !== 'active')) {\n return 'open';\n }\n if (status === 'active') {\n return 'member';\n }\n if (status === 'requested' || status === 'invited') {\n return status;\n }\n if (space.capacity !== null && memberCount >= space.capacity) {\n return 'full';\n }\n return 'none';\n}\n"],"mappings":"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"}
|
package/dist/shared/errors.d.ts
CHANGED
|
@@ -23,6 +23,18 @@ declare class ForbiddenError extends Error {
|
|
|
23
23
|
declare class InvalidInputError extends Error {
|
|
24
24
|
constructor(message: string);
|
|
25
25
|
}
|
|
26
|
+
declare class UnavailableError extends Error {
|
|
27
|
+
readonly details?: Record<string, string> | undefined;
|
|
28
|
+
constructor(message: string, details?: Record<string, string> | undefined);
|
|
29
|
+
}
|
|
30
|
+
type ValidationProblem = {
|
|
31
|
+
path?: string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
declare class ValidationError extends Error {
|
|
35
|
+
readonly problems: ValidationProblem[];
|
|
36
|
+
constructor(problems: string | ValidationProblem[]);
|
|
37
|
+
}
|
|
26
38
|
//#endregion
|
|
27
|
-
export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError };
|
|
39
|
+
export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError, ValidationProblem };
|
|
28
40
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/shared/errors.js
CHANGED
|
@@ -37,7 +37,24 @@ var InvalidInputError = class extends Error {
|
|
|
37
37
|
this.name = "InvalidInputError";
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
+
var UnavailableError = class extends Error {
|
|
41
|
+
details;
|
|
42
|
+
constructor(message, details) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.details = details;
|
|
45
|
+
this.name = "UnavailableError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var ValidationError = class extends Error {
|
|
49
|
+
problems;
|
|
50
|
+
constructor(problems) {
|
|
51
|
+
const list = typeof problems === "string" ? [{ message: problems }] : problems;
|
|
52
|
+
super(list.map((problem) => problem.path ? `${problem.path}: ${problem.message}` : problem.message).join("; "));
|
|
53
|
+
this.problems = list;
|
|
54
|
+
this.name = "ValidationError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
40
57
|
//#endregion
|
|
41
|
-
export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError };
|
|
58
|
+
export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError };
|
|
42
59
|
|
|
43
60
|
//# sourceMappingURL=errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../src/shared/errors.ts"],"sourcesContent":["// Cross-cutting domain errors. Like `../shared/ports.js`, this file is\n// importable from any context; the central HTTP error handler maps these to\n// status codes so routes never hand-roll error replies.\n\n/** A command's target does not exist in this org. Queries return null for the\n * same situation — a missing row is a normal answer to a question, but a\n * failed precondition for a command. */\nexport class NotFoundError extends Error {\n constructor(\n readonly resource: string,\n readonly id: string,\n ) {\n super(`${resource} not found`);\n this.name = 'NotFoundError';\n }\n}\n\n/** A command conflicts with existing state (duplicate email, already linked,\n * …). The HTTP layer maps this to 409. */\nexport class ConflictError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConflictError';\n }\n}\n\n/** The caller is authenticated but not permitted to perform this command.\n * The HTTP layer maps this to 403. */\nexport class ForbiddenError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ForbiddenError';\n }\n}\n\n/** The caller's own input breaks a rule, independent of existing state\n * (wrong file type, too large, talking to yourself, …). The HTTP layer maps\n * this to 400. */\nexport class InvalidInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidInputError';\n }\n}\n"],"mappings":";;;;AAOA,IAAa,gBAAb,cAAmC,MAAM;CAE5B;CACA;CAFX,YACE,UACA,IACA;EACA,MAAM,GAAG,SAAS,WAAW;EAHpB,KAAA,WAAA;EACA,KAAA,KAAA;EAGT,KAAK,OAAO;CACd;AACF;;;AAIA,IAAa,gBAAb,cAAmC,MAAM;CACvC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;AAIA,IAAa,iBAAb,cAAoC,MAAM;CACxC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/shared/errors.ts"],"sourcesContent":["// Cross-cutting domain errors. Like `../shared/ports.js`, this file is\n// importable from any context; the central HTTP error handler maps these to\n// status codes so routes never hand-roll error replies.\n\n/** A command's target does not exist in this org. Queries return null for the\n * same situation — a missing row is a normal answer to a question, but a\n * failed precondition for a command. */\nexport class NotFoundError extends Error {\n constructor(\n readonly resource: string,\n readonly id: string,\n ) {\n super(`${resource} not found`);\n this.name = 'NotFoundError';\n }\n}\n\n/** A command conflicts with existing state (duplicate email, already linked,\n * …). The HTTP layer maps this to 409. */\nexport class ConflictError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConflictError';\n }\n}\n\n/** The caller is authenticated but not permitted to perform this command.\n * The HTTP layer maps this to 403. */\nexport class ForbiddenError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ForbiddenError';\n }\n}\n\n/** The caller's own input breaks a rule, independent of existing state\n * (wrong file type, too large, talking to yourself, …). The HTTP layer maps\n * this to 400. */\nexport class InvalidInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidInputError';\n }\n}\n\nexport class UnavailableError extends Error {\n constructor(\n message: string,\n readonly details?: Record<string, string>,\n ) {\n super(message);\n this.name = 'UnavailableError';\n }\n}\n\nexport type ValidationProblem = { path?: string; message: string };\n\nexport class ValidationError extends Error {\n readonly problems: ValidationProblem[];\n\n constructor(problems: string | ValidationProblem[]) {\n const list = typeof problems === 'string' ? [{ message: problems }] : problems;\n super(\n list\n .map((problem) => (problem.path ? `${problem.path}: ${problem.message}` : problem.message))\n .join('; '),\n );\n this.problems = list;\n this.name = 'ValidationError';\n }\n}\n"],"mappings":";;;;AAOA,IAAa,gBAAb,cAAmC,MAAM;CAE5B;CACA;CAFX,YACE,UACA,IACA;EACA,MAAM,GAAG,SAAS,WAAW;EAHpB,KAAA,WAAA;EACA,KAAA,KAAA;EAGT,KAAK,OAAO;CACd;AACF;;;AAIA,IAAa,gBAAb,cAAmC,MAAM;CACvC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;AAIA,IAAa,iBAAb,cAAoC,MAAM;CACxC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;;;;AAKA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CAG/B;CAFX,YACE,SACA,SACA;EACA,MAAM,OAAO;EAFJ,KAAA,UAAA;EAGT,KAAK,OAAO;CACd;AACF;AAIA,IAAa,kBAAb,cAAqC,MAAM;CACzC;CAEA,YAAY,UAAwC;EAClD,MAAM,OAAO,OAAO,aAAa,WAAW,CAAC,EAAE,SAAS,SAAS,CAAC,IAAI;EACtE,MACE,KACG,KAAK,YAAa,QAAQ,OAAO,GAAG,QAAQ,KAAK,IAAI,QAAQ,YAAY,QAAQ,OAAQ,CAAC,CAC1F,KAAK,IAAI,CACd;EACA,KAAK,WAAW;EAChB,KAAK,OAAO;CACd;AACF"}
|
package/dist/shared/id.d.ts
CHANGED
|
@@ -41,6 +41,9 @@ declare const ID_PREFIXES: {
|
|
|
41
41
|
readonly message: "msg";
|
|
42
42
|
readonly messageFlag: "mfl";
|
|
43
43
|
readonly notification: "ntf";
|
|
44
|
+
readonly notificationSetting: "nst";
|
|
45
|
+
readonly notificationDelivery: "ndl";
|
|
46
|
+
readonly device: "dev";
|
|
44
47
|
};
|
|
45
48
|
type IdType = keyof typeof ID_PREFIXES;
|
|
46
49
|
/** A bare KSUID body (27-char base62, time-sortable), no prefix. */
|
package/dist/shared/id.js
CHANGED
|
@@ -41,7 +41,10 @@ const ID_PREFIXES = {
|
|
|
41
41
|
chat: "cht",
|
|
42
42
|
message: "msg",
|
|
43
43
|
messageFlag: "mfl",
|
|
44
|
-
notification: "ntf"
|
|
44
|
+
notification: "ntf",
|
|
45
|
+
notificationSetting: "nst",
|
|
46
|
+
notificationDelivery: "ndl",
|
|
47
|
+
device: "dev"
|
|
45
48
|
};
|
|
46
49
|
/** A bare KSUID body (27-char base62, time-sortable), no prefix. */
|
|
47
50
|
function ksuid() {
|
package/dist/shared/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","names":[],"sources":["../../src/shared/id.ts"],"sourcesContent":["import KSUID from 'ksuid';\n\n/**\n * Prefixed, KSUID-bodied string ids — `${prefix}_${ksuid}`, e.g. `org_2QZ4mK...`.\n *\n * The body is a real KSUID (27-char base62, 4-byte timestamp + 16 random bytes),\n * so ids are lexicographically time-sortable. The prefix marks the entity type at\n * a glance. A core utility (one implementation, nothing to swap), called directly\n * by core services and used as the Drizzle `.$defaultFn` for every domain id column.\n */\nexport const ID_PREFIXES = {\n organization: 'org',\n user: 'usr',\n student: 'stu',\n orgUser: 'orm',\n invite: 'ivt',\n bundle: 'bun',\n course: 'crs',\n module: 'mod',\n activity: 'act',\n activityAsset: 'aca',\n download: 'dwn',\n downloadAsset: 'dla',\n asset: 'ast',\n entitlement: 'ent',\n progress: 'prg',\n credential: 'crd',\n connection: 'con',\n event: 'evt',\n automation: 'atm',\n automationRun: 'run',\n comment: 'cmt',\n commentReport: 'crp',\n community: 'cmy',\n space: 'spc',\n topic: 'top',\n spaceLink: 'slk',\n spaceMember: 'spm',\n post: 'pst',\n postFlag: 'pfl',\n chat: 'cht',\n message: 'msg',\n messageFlag: 'mfl',\n notification: 'ntf',\n} as const;\n\nexport type IdType = keyof typeof ID_PREFIXES;\n\n/** A bare KSUID body (27-char base62, time-sortable), no prefix. */\nexport function ksuid(): string {\n return KSUID.randomSync().string;\n}\n\n/** Attach an arbitrary prefix to a fresh KSUID — `${prefix}_${ksuid}`. */\nexport function prefixId(prefix: string): string {\n return `${prefix}_${ksuid()}`;\n}\n\n/** Generate a prefixed, time-sortable id for the given domain entity type. */\nexport function genId(type: IdType): string {\n return prefixId(ID_PREFIXES[type]);\n}\n\n/** True when `value` carries the prefix for `type` (shape check only, not existence). */\nexport function isId(type: IdType, value: string): boolean {\n return value.startsWith(`${ID_PREFIXES[type]}_`);\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAa,cAAc;CACzB,cAAc;CACd,MAAM;CACN,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,UAAU;CACV,eAAe;CACf,OAAO;CACP,aAAa;CACb,UAAU;CACV,YAAY;CACZ,YAAY;CACZ,OAAO;CACP,YAAY;CACZ,eAAe;CACf,SAAS;CACT,eAAe;CACf,WAAW;CACX,OAAO;CACP,OAAO;CACP,WAAW;CACX,aAAa;CACb,MAAM;CACN,UAAU;CACV,MAAM;CACN,SAAS;CACT,aAAa;CACb,cAAc;
|
|
1
|
+
{"version":3,"file":"id.js","names":[],"sources":["../../src/shared/id.ts"],"sourcesContent":["import KSUID from 'ksuid';\n\n/**\n * Prefixed, KSUID-bodied string ids — `${prefix}_${ksuid}`, e.g. `org_2QZ4mK...`.\n *\n * The body is a real KSUID (27-char base62, 4-byte timestamp + 16 random bytes),\n * so ids are lexicographically time-sortable. The prefix marks the entity type at\n * a glance. A core utility (one implementation, nothing to swap), called directly\n * by core services and used as the Drizzle `.$defaultFn` for every domain id column.\n */\nexport const ID_PREFIXES = {\n organization: 'org',\n user: 'usr',\n student: 'stu',\n orgUser: 'orm',\n invite: 'ivt',\n bundle: 'bun',\n course: 'crs',\n module: 'mod',\n activity: 'act',\n activityAsset: 'aca',\n download: 'dwn',\n downloadAsset: 'dla',\n asset: 'ast',\n entitlement: 'ent',\n progress: 'prg',\n credential: 'crd',\n connection: 'con',\n event: 'evt',\n automation: 'atm',\n automationRun: 'run',\n comment: 'cmt',\n commentReport: 'crp',\n community: 'cmy',\n space: 'spc',\n topic: 'top',\n spaceLink: 'slk',\n spaceMember: 'spm',\n post: 'pst',\n postFlag: 'pfl',\n chat: 'cht',\n message: 'msg',\n messageFlag: 'mfl',\n notification: 'ntf',\n notificationSetting: 'nst',\n notificationDelivery: 'ndl',\n device: 'dev',\n} as const;\n\nexport type IdType = keyof typeof ID_PREFIXES;\n\n/** A bare KSUID body (27-char base62, time-sortable), no prefix. */\nexport function ksuid(): string {\n return KSUID.randomSync().string;\n}\n\n/** Attach an arbitrary prefix to a fresh KSUID — `${prefix}_${ksuid}`. */\nexport function prefixId(prefix: string): string {\n return `${prefix}_${ksuid()}`;\n}\n\n/** Generate a prefixed, time-sortable id for the given domain entity type. */\nexport function genId(type: IdType): string {\n return prefixId(ID_PREFIXES[type]);\n}\n\n/** True when `value` carries the prefix for `type` (shape check only, not existence). */\nexport function isId(type: IdType, value: string): boolean {\n return value.startsWith(`${ID_PREFIXES[type]}_`);\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAa,cAAc;CACzB,cAAc;CACd,MAAM;CACN,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,UAAU;CACV,eAAe;CACf,OAAO;CACP,aAAa;CACb,UAAU;CACV,YAAY;CACZ,YAAY;CACZ,OAAO;CACP,YAAY;CACZ,eAAe;CACf,SAAS;CACT,eAAe;CACf,WAAW;CACX,OAAO;CACP,OAAO;CACP,WAAW;CACX,aAAa;CACb,MAAM;CACN,UAAU;CACV,MAAM;CACN,SAAS;CACT,aAAa;CACb,cAAc;CACd,qBAAqB;CACrB,sBAAsB;CACtB,QAAQ;AACV;;AAKA,SAAgB,QAAgB;CAC9B,OAAO,MAAM,WAAW,CAAC,CAAC;AAC5B;;AAGA,SAAgB,SAAS,QAAwB;CAC/C,OAAO,GAAG,OAAO,GAAG,MAAM;AAC5B;;AAGA,SAAgB,MAAM,MAAsB;CAC1C,OAAO,SAAS,YAAY,KAAK;AACnC;;AAGA,SAAgB,KAAK,MAAc,OAAwB;CACzD,OAAO,MAAM,WAAW,GAAG,YAAY,MAAM,EAAE;AACjD"}
|
package/dist/shared/mailer.d.ts
CHANGED
|
@@ -16,6 +16,16 @@ interface MailerLookups {
|
|
|
16
16
|
type: ContentType;
|
|
17
17
|
} | null>;
|
|
18
18
|
}
|
|
19
|
+
declare class EmailRejectedError extends Error {
|
|
20
|
+
readonly status?: number | undefined;
|
|
21
|
+
constructor(message: string, status?: number | undefined);
|
|
22
|
+
}
|
|
23
|
+
declare function isEmailRejected(err: unknown): err is EmailRejectedError;
|
|
24
|
+
declare class EmailRenderError extends Error {
|
|
25
|
+
readonly template: string;
|
|
26
|
+
constructor(template: string, cause: unknown);
|
|
27
|
+
}
|
|
28
|
+
declare function isEmailRenderError(err: unknown): err is EmailRenderError;
|
|
19
29
|
declare class Mailer {
|
|
20
30
|
private readonly templates;
|
|
21
31
|
private readonly email;
|
|
@@ -24,5 +34,5 @@ declare class Mailer {
|
|
|
24
34
|
send<K extends EmailTemplateId>(to: string, id: K, params: EmailTemplateParams[K], ctx?: Partial<TemplateContext>): Promise<void>;
|
|
25
35
|
}
|
|
26
36
|
//#endregion
|
|
27
|
-
export { Mailer, MailerLookups };
|
|
37
|
+
export { EmailRejectedError, EmailRenderError, Mailer, MailerLookups, isEmailRejected, isEmailRenderError };
|
|
28
38
|
//# sourceMappingURL=mailer.d.ts.map
|
package/dist/shared/mailer.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
//#region src/shared/mailer.ts
|
|
2
|
+
var EmailRejectedError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "EmailRejectedError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function isEmailRejected(err) {
|
|
11
|
+
return err instanceof Error && err.name === "EmailRejectedError";
|
|
12
|
+
}
|
|
13
|
+
var EmailRenderError = class extends Error {
|
|
14
|
+
template;
|
|
15
|
+
constructor(template, cause) {
|
|
16
|
+
super(`email template ${template} failed to render: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
17
|
+
this.template = template;
|
|
18
|
+
this.name = "EmailRenderError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function isEmailRenderError(err) {
|
|
22
|
+
return err instanceof Error && err.name === "EmailRenderError";
|
|
23
|
+
}
|
|
2
24
|
var Mailer = class {
|
|
3
25
|
templates;
|
|
4
26
|
email;
|
|
@@ -9,10 +31,15 @@ var Mailer = class {
|
|
|
9
31
|
this.ctx = ctx;
|
|
10
32
|
}
|
|
11
33
|
async send(to, id, params, ctx) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
let content;
|
|
35
|
+
try {
|
|
36
|
+
content = await this.templates.render(id, {
|
|
37
|
+
...this.ctx,
|
|
38
|
+
...ctx
|
|
39
|
+
}, params);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new EmailRenderError(id, err);
|
|
42
|
+
}
|
|
16
43
|
await this.email.send({
|
|
17
44
|
to,
|
|
18
45
|
subject: content.subject,
|
|
@@ -22,6 +49,6 @@ var Mailer = class {
|
|
|
22
49
|
}
|
|
23
50
|
};
|
|
24
51
|
//#endregion
|
|
25
|
-
export { Mailer };
|
|
52
|
+
export { EmailRejectedError, EmailRenderError, Mailer, isEmailRejected, isEmailRenderError };
|
|
26
53
|
|
|
27
54
|
//# sourceMappingURL=mailer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailer.js","names":[],"sources":["../../src/shared/mailer.ts"],"sourcesContent":["// Composes a TemplateRenderer with an EmailSender: callers name a template,\n// the mailer resolves content and hands it to the transport.\nimport type {\n EmailSender,\n EmailTemplateId,\n EmailTemplateParams,\n TemplateContext,\n TemplateRenderer,\n} from './ports.js';\nimport type { ContentType } from '../types/schemas/content.js';\n\n/** Resolves the recipient/content details a template needs when the\n * triggering event carries only row ids. Wired by the app layer. */\nexport interface MailerLookups {\n orgUserPerson(\n orgId: string,\n orgUserId: string,\n ): Promise<{ userId: string; email: string } | null>;\n contentInfo(\n orgId: string,\n contentId: string,\n ): Promise<{ id: string; title: string; type: ContentType } | null>;\n}\n\nexport class Mailer {\n constructor(\n private readonly templates: TemplateRenderer,\n private readonly email: EmailSender,\n private readonly ctx: TemplateContext,\n ) {}\n\n async send<K extends EmailTemplateId>(\n to: string,\n id: K,\n params: EmailTemplateParams[K],\n ctx?: Partial<TemplateContext>,\n ): Promise<void> {\n
|
|
1
|
+
{"version":3,"file":"mailer.js","names":[],"sources":["../../src/shared/mailer.ts"],"sourcesContent":["// Composes a TemplateRenderer with an EmailSender: callers name a template,\n// the mailer resolves content and hands it to the transport.\nimport type {\n EmailSender,\n EmailTemplateId,\n EmailTemplateParams,\n TemplateContext,\n TemplateRenderer,\n} from './ports.js';\nimport type { ContentType } from '../types/schemas/content.js';\n\n/** Resolves the recipient/content details a template needs when the\n * triggering event carries only row ids. Wired by the app layer. */\nexport interface MailerLookups {\n orgUserPerson(\n orgId: string,\n orgUserId: string,\n ): Promise<{ userId: string; email: string } | null>;\n contentInfo(\n orgId: string,\n contentId: string,\n ): Promise<{ id: string; title: string; type: ContentType } | null>;\n}\n\nexport class EmailRejectedError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n ) {\n super(message);\n this.name = 'EmailRejectedError';\n }\n}\n\nexport function isEmailRejected(err: unknown): err is EmailRejectedError {\n return err instanceof Error && err.name === 'EmailRejectedError';\n}\n\nexport class EmailRenderError extends Error {\n constructor(\n readonly template: string,\n cause: unknown,\n ) {\n super(\n `email template ${template} failed to render: ${cause instanceof Error ? cause.message : String(cause)}`,\n { cause },\n );\n this.name = 'EmailRenderError';\n }\n}\n\nexport function isEmailRenderError(err: unknown): err is EmailRenderError {\n return err instanceof Error && err.name === 'EmailRenderError';\n}\n\nexport class Mailer {\n constructor(\n private readonly templates: TemplateRenderer,\n private readonly email: EmailSender,\n private readonly ctx: TemplateContext,\n ) {}\n\n async send<K extends EmailTemplateId>(\n to: string,\n id: K,\n params: EmailTemplateParams[K],\n ctx?: Partial<TemplateContext>,\n ): Promise<void> {\n let content: Awaited<ReturnType<TemplateRenderer['render']>>;\n try {\n content = await this.templates.render(id, { ...this.ctx, ...ctx }, params);\n } catch (err) {\n throw new EmailRenderError(id, err);\n }\n await this.email.send({ to, subject: content.subject, text: content.text, html: content.html });\n }\n}\n"],"mappings":";AAwBA,IAAa,qBAAb,cAAwC,MAAM;CAGjC;CAFX,YACE,SACA,QACA;EACA,MAAM,OAAO;EAFJ,KAAA,SAAA;EAGT,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,gBAAgB,KAAyC;CACvE,OAAO,eAAe,SAAS,IAAI,SAAS;AAC9C;AAEA,IAAa,mBAAb,cAAsC,MAAM;CAE/B;CADX,YACE,UACA,OACA;EACA,MACE,kBAAkB,SAAS,qBAAqB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KACrG,EAAE,MAAM,CACV;EANS,KAAA,WAAA;EAOT,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,mBAAmB,KAAuC;CACxE,OAAO,eAAe,SAAS,IAAI,SAAS;AAC9C;AAEA,IAAa,SAAb,MAAoB;CAEC;CACA;CACA;CAHnB,YACE,WACA,OACA,KACA;EAHiB,KAAA,YAAA;EACA,KAAA,QAAA;EACA,KAAA,MAAA;CAChB;CAEH,MAAM,KACJ,IACA,IACA,QACA,KACe;EACf,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,KAAK,UAAU,OAAO,IAAI;IAAE,GAAG,KAAK;IAAK,GAAG;GAAI,GAAG,MAAM;EAC3E,SAAS,KAAK;GACZ,MAAM,IAAI,iBAAiB,IAAI,GAAG;EACpC;EACA,MAAM,KAAK,MAAM,KAAK;GAAE;GAAI,SAAS,QAAQ;GAAS,MAAM,QAAQ;GAAM,MAAM,QAAQ;EAAK,CAAC;CAChG;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NotificationCategory, NotificationTypeDeclaration } from "../types/notifications.js";
|
|
2
|
+
import "../types/index.js";
|
|
3
|
+
import { NotificationTypeRegistry } from "./ports.js";
|
|
4
|
+
import { ValidationProblem } from "./errors.js";
|
|
5
|
+
//#region src/shared/notification-categories.d.ts
|
|
6
|
+
type CategoryListScope = 'org' | 'community';
|
|
7
|
+
declare function coversListScope(declaration: Pick<NotificationTypeDeclaration, 'scopes'>, listScope: CategoryListScope): boolean;
|
|
8
|
+
declare function typesForList(types: Pick<NotificationTypeRegistry, 'list'>, listScope: CategoryListScope): Pick<NotificationTypeDeclaration, 'type' | 'scopes' | 'reasons'>[];
|
|
9
|
+
declare function categoryListProblems(categories: NotificationCategory[], listScope: CategoryListScope, types: Pick<NotificationTypeRegistry, 'get'>, configured: string[]): ValidationProblem[];
|
|
10
|
+
declare function assertValidCategoryList(categories: NotificationCategory[], listScope: CategoryListScope, types: Pick<NotificationTypeRegistry, 'get'>, configured: string[]): void;
|
|
11
|
+
declare function channelOrder(configured: string[]): string[];
|
|
12
|
+
declare function offeringConfigured(categories: NotificationCategory[], configured: string[]): NotificationCategory[];
|
|
13
|
+
declare function storedCategoryList(value: unknown): NotificationCategory[] | null;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { CategoryListScope, assertValidCategoryList, categoryListProblems, channelOrder, coversListScope, offeringConfigured, storedCategoryList, typesForList };
|
|
16
|
+
//# sourceMappingURL=notification-categories.d.ts.map
|