@headless-lms/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/assets/events.d.ts +42 -24
  2. package/dist/assets/events.js +1 -1
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +6 -4
  8. package/dist/assets/index.js +4 -2
  9. package/dist/assets/jobs.d.ts +15 -0
  10. package/dist/assets/jobs.js +30 -0
  11. package/dist/assets/jobs.js.map +1 -0
  12. package/dist/assets/model.d.ts +2 -2
  13. package/dist/assets/ports.d.ts +46 -15
  14. package/dist/assets/service.d.ts +38 -7
  15. package/dist/assets/service.js +287 -38
  16. package/dist/assets/service.js.map +1 -1
  17. package/dist/automations/actions.d.ts +13 -24
  18. package/dist/automations/actions.js +106 -42
  19. package/dist/automations/actions.js.map +1 -1
  20. package/dist/automations/catalog.js +226 -3
  21. package/dist/automations/catalog.js.map +1 -1
  22. package/dist/automations/community-derivations.d.ts +14 -0
  23. package/dist/automations/community-derivations.js +439 -0
  24. package/dist/automations/community-derivations.js.map +1 -0
  25. package/dist/automations/community-mail.d.ts +98 -0
  26. package/dist/automations/community-mail.js +70 -0
  27. package/dist/automations/community-mail.js.map +1 -0
  28. package/dist/automations/events.d.ts +18 -0
  29. package/dist/automations/index.d.ts +13 -4
  30. package/dist/automations/index.js +10 -2
  31. package/dist/automations/model.d.ts +10 -3
  32. package/dist/automations/model.js +15 -2
  33. package/dist/automations/model.js.map +1 -1
  34. package/dist/automations/notifications/debounce.d.ts +25 -0
  35. package/dist/automations/notifications/debounce.js +93 -0
  36. package/dist/automations/notifications/debounce.js.map +1 -0
  37. package/dist/automations/notifications/defaults.d.ts +8 -0
  38. package/dist/automations/notifications/defaults.js +316 -0
  39. package/dist/automations/notifications/defaults.js.map +1 -0
  40. package/dist/automations/notifications/delivery.d.ts +28 -0
  41. package/dist/automations/notifications/delivery.js +88 -0
  42. package/dist/automations/notifications/delivery.js.map +1 -0
  43. package/dist/automations/notifications/processor.d.ts +64 -0
  44. package/dist/automations/notifications/processor.js +270 -0
  45. package/dist/automations/notifications/processor.js.map +1 -0
  46. package/dist/automations/notifications/rules/content.d.ts +6 -0
  47. package/dist/automations/notifications/rules/content.js +39 -0
  48. package/dist/automations/notifications/rules/content.js.map +1 -0
  49. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  50. package/dist/automations/notifications/rules/discussion.js +678 -0
  51. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  52. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  53. package/dist/automations/notifications/rules/entitlements.js +249 -0
  54. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  55. package/dist/automations/notifications/rules/index.d.ts +6 -0
  56. package/dist/automations/notifications/rules/index.js +15 -0
  57. package/dist/automations/notifications/rules/index.js.map +1 -0
  58. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  59. package/dist/automations/notifications/rules/organizations.js +28 -0
  60. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  61. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  62. package/dist/automations/notifications/rules/recipients.js +99 -0
  63. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  64. package/dist/automations/notifications/service.d.ts +27 -0
  65. package/dist/automations/notifications/service.js +45 -0
  66. package/dist/automations/notifications/service.js.map +1 -0
  67. package/dist/automations/notifications/types.d.ts +7 -0
  68. package/dist/automations/notifications/types.js +473 -0
  69. package/dist/automations/notifications/types.js.map +1 -0
  70. package/dist/automations/ports.d.ts +121 -3
  71. package/dist/automations/schedule.d.ts +6 -0
  72. package/dist/automations/schedule.js +41 -0
  73. package/dist/automations/schedule.js.map +1 -0
  74. package/dist/automations/service.d.ts +20 -4
  75. package/dist/automations/service.js +246 -45
  76. package/dist/automations/service.js.map +1 -1
  77. package/dist/content/events.d.ts +712 -50
  78. package/dist/content/events.js +55 -1
  79. package/dist/content/events.js.map +1 -1
  80. package/dist/content/index.d.ts +6 -4
  81. package/dist/content/index.js +3 -1
  82. package/dist/content/model.d.ts +2 -2
  83. package/dist/content/notification-categories.d.ts +36 -0
  84. package/dist/content/notification-categories.js +64 -0
  85. package/dist/content/notification-categories.js.map +1 -0
  86. package/dist/content/ports.d.ts +90 -18
  87. package/dist/content/service.d.ts +57 -18
  88. package/dist/content/service.js +520 -25
  89. package/dist/content/service.js.map +1 -1
  90. package/dist/content/titles.d.ts +22 -0
  91. package/dist/content/titles.js +69 -0
  92. package/dist/content/titles.js.map +1 -0
  93. package/dist/content/types.d.ts +2 -2
  94. package/dist/discussion/authors.d.ts +10 -0
  95. package/dist/discussion/authors.js +23 -0
  96. package/dist/discussion/authors.js.map +1 -0
  97. package/dist/discussion/chats.d.ts +109 -0
  98. package/dist/discussion/chats.js +831 -0
  99. package/dist/discussion/chats.js.map +1 -0
  100. package/dist/discussion/digests.d.ts +24 -0
  101. package/dist/discussion/digests.js +104 -0
  102. package/dist/discussion/digests.js.map +1 -0
  103. package/dist/discussion/events.d.ts +785 -56
  104. package/dist/discussion/events.js +165 -5
  105. package/dist/discussion/events.js.map +1 -1
  106. package/dist/discussion/index.d.ts +12 -3
  107. package/dist/discussion/index.js +9 -1
  108. package/dist/discussion/jobs.d.ts +16 -0
  109. package/dist/discussion/jobs.js +40 -0
  110. package/dist/discussion/jobs.js.map +1 -0
  111. package/dist/discussion/mentions.d.ts +11 -0
  112. package/dist/discussion/mentions.js +35 -0
  113. package/dist/discussion/mentions.js.map +1 -0
  114. package/dist/discussion/ports.d.ts +372 -28
  115. package/dist/discussion/ports.js +6 -1
  116. package/dist/discussion/ports.js.map +1 -0
  117. package/dist/discussion/posts.d.ts +113 -0
  118. package/dist/discussion/posts.js +775 -0
  119. package/dist/discussion/posts.js.map +1 -0
  120. package/dist/discussion/service.d.ts +42 -11
  121. package/dist/discussion/service.js +364 -85
  122. package/dist/discussion/service.js.map +1 -1
  123. package/dist/discussion/subscriptions.d.ts +37 -0
  124. package/dist/discussion/subscriptions.js +99 -0
  125. package/dist/discussion/subscriptions.js.map +1 -0
  126. package/dist/discussion/titles.d.ts +26 -0
  127. package/dist/discussion/titles.js +84 -0
  128. package/dist/discussion/titles.js.map +1 -0
  129. package/dist/entitlements/events.d.ts +178 -11
  130. package/dist/entitlements/events.js +28 -1
  131. package/dist/entitlements/events.js.map +1 -1
  132. package/dist/entitlements/index.d.ts +6 -4
  133. package/dist/entitlements/index.js +3 -1
  134. package/dist/entitlements/jobs.d.ts +12 -0
  135. package/dist/entitlements/jobs.js +18 -0
  136. package/dist/entitlements/jobs.js.map +1 -0
  137. package/dist/entitlements/model.d.ts +3 -2
  138. package/dist/entitlements/ports.d.ts +128 -11
  139. package/dist/entitlements/rules.d.ts +12 -0
  140. package/dist/entitlements/rules.js +17 -0
  141. package/dist/entitlements/rules.js.map +1 -0
  142. package/dist/entitlements/service.d.ts +57 -3
  143. package/dist/entitlements/service.js +492 -2
  144. package/dist/entitlements/service.js.map +1 -1
  145. package/dist/identity/access.d.ts +6 -0
  146. package/dist/identity/access.js +7 -0
  147. package/dist/identity/access.js.map +1 -0
  148. package/dist/identity/devices.d.ts +30 -0
  149. package/dist/identity/devices.js +107 -0
  150. package/dist/identity/devices.js.map +1 -0
  151. package/dist/identity/events.d.ts +2 -2
  152. package/dist/identity/index.d.ts +7 -4
  153. package/dist/identity/index.js +4 -1
  154. package/dist/identity/model.d.ts +2 -1
  155. package/dist/identity/notification-settings.d.ts +28 -0
  156. package/dist/identity/notification-settings.js +113 -0
  157. package/dist/identity/notification-settings.js.map +1 -0
  158. package/dist/identity/ports.d.ts +97 -3
  159. package/dist/identity/service.d.ts +14 -3
  160. package/dist/identity/service.js +62 -5
  161. package/dist/identity/service.js.map +1 -1
  162. package/dist/integrations/events.d.ts +3 -3
  163. package/dist/integrations/model.js +1 -1
  164. package/dist/integrations/model.js.map +1 -1
  165. package/dist/integrations/service.js +2 -2
  166. package/dist/integrations/service.js.map +1 -1
  167. package/dist/organizations/access-scopes.d.ts +19 -0
  168. package/dist/organizations/access-scopes.js +89 -0
  169. package/dist/organizations/access-scopes.js.map +1 -0
  170. package/dist/organizations/events.d.ts +75 -33
  171. package/dist/organizations/events.js +6 -0
  172. package/dist/organizations/events.js.map +1 -1
  173. package/dist/organizations/index.d.ts +8 -6
  174. package/dist/organizations/index.js +5 -3
  175. package/dist/organizations/members.d.ts +8 -5
  176. package/dist/organizations/members.js.map +1 -1
  177. package/dist/organizations/notification-categories.d.ts +32 -0
  178. package/dist/organizations/notification-categories.js +51 -0
  179. package/dist/organizations/notification-categories.js.map +1 -0
  180. package/dist/organizations/ports.d.ts +17 -11
  181. package/dist/organizations/roles.d.ts +6 -10
  182. package/dist/organizations/roles.js +19 -7
  183. package/dist/organizations/roles.js.map +1 -1
  184. package/dist/organizations/service.d.ts +22 -11
  185. package/dist/organizations/service.js +164 -47
  186. package/dist/organizations/service.js.map +1 -1
  187. package/dist/organizations/types.d.ts +1 -10
  188. package/dist/progress/events.d.ts +14 -14
  189. package/dist/progress/index.d.ts +2 -2
  190. package/dist/progress/ports.d.ts +6 -1
  191. package/dist/progress/service.d.ts +3 -1
  192. package/dist/progress/service.js +6 -2
  193. package/dist/progress/service.js.map +1 -1
  194. package/dist/reporting/admin/index.d.ts +4 -0
  195. package/dist/reporting/admin/index.js +2 -0
  196. package/dist/reporting/admin/model.d.ts +108 -0
  197. package/dist/reporting/admin/ports.d.ts +27 -0
  198. package/dist/reporting/admin/service.d.ts +22 -0
  199. package/dist/reporting/admin/service.js +38 -0
  200. package/dist/reporting/admin/service.js.map +1 -0
  201. package/dist/reporting/member/index.d.ts +5 -0
  202. package/dist/reporting/member/index.js +2 -0
  203. package/dist/reporting/member/model.d.ts +157 -0
  204. package/dist/reporting/member/ports.d.ts +48 -0
  205. package/dist/reporting/member/service.d.ts +92 -0
  206. package/dist/reporting/member/service.js +364 -0
  207. package/dist/reporting/member/service.js.map +1 -0
  208. package/dist/shared/actor.d.ts +9 -0
  209. package/dist/shared/errors.d.ts +19 -1
  210. package/dist/shared/errors.js +27 -1
  211. package/dist/shared/errors.js.map +1 -1
  212. package/dist/shared/id.d.ts +14 -0
  213. package/dist/shared/id.js +15 -1
  214. package/dist/shared/id.js.map +1 -1
  215. package/dist/shared/mailer.d.ts +15 -2
  216. package/dist/shared/mailer.js +32 -5
  217. package/dist/shared/mailer.js.map +1 -1
  218. package/dist/shared/network.d.ts +10 -0
  219. package/dist/shared/network.js +211 -0
  220. package/dist/shared/network.js.map +1 -0
  221. package/dist/shared/notification-categories.d.ts +16 -0
  222. package/dist/shared/notification-categories.js +94 -0
  223. package/dist/shared/notification-categories.js.map +1 -0
  224. package/dist/shared/notification-settings.d.ts +14 -0
  225. package/dist/shared/notification-settings.js +61 -0
  226. package/dist/shared/notification-settings.js.map +1 -0
  227. package/dist/shared/ports.d.ts +48 -2
  228. package/dist/shared/settings.d.ts +2 -1
  229. package/dist/shared/settings.js +1 -0
  230. package/dist/shared/settings.js.map +1 -1
  231. package/dist/types/assets.d.ts +2 -2
  232. package/dist/types/automations.d.ts +15 -1
  233. package/dist/types/content.d.ts +2 -2
  234. package/dist/types/discussion.d.ts +11 -3
  235. package/dist/types/email-templates.d.ts +216 -1
  236. package/dist/types/entitlements.d.ts +2 -2
  237. package/dist/types/identity.d.ts +48 -2
  238. package/dist/types/index.d.ts +9 -8
  239. package/dist/types/notifications.d.ts +157 -0
  240. package/dist/types/ports.d.ts +6 -0
  241. package/dist/types/schemas/assets.d.ts +65 -37
  242. package/dist/types/schemas/assets.js +17 -12
  243. package/dist/types/schemas/assets.js.map +1 -1
  244. package/dist/types/schemas/automations.d.ts +9 -1
  245. package/dist/types/schemas/automations.js +13 -6
  246. package/dist/types/schemas/automations.js.map +1 -1
  247. package/dist/types/schemas/content.d.ts +396 -3
  248. package/dist/types/schemas/content.js +161 -3
  249. package/dist/types/schemas/content.js.map +1 -1
  250. package/dist/types/schemas/discussion.d.ts +443 -3
  251. package/dist/types/schemas/discussion.js +233 -4
  252. package/dist/types/schemas/discussion.js.map +1 -1
  253. package/dist/types/schemas/entitlements.d.ts +36 -2
  254. package/dist/types/schemas/entitlements.js +23 -1
  255. package/dist/types/schemas/entitlements.js.map +1 -1
  256. package/dist/types/schemas/identity.d.ts +27 -1
  257. package/dist/types/schemas/identity.js +14 -2
  258. package/dist/types/schemas/identity.js.map +1 -1
  259. package/dist/types/schemas/index.d.ts +9 -8
  260. package/dist/types/schemas/index.js +9 -8
  261. package/dist/types/schemas/notifications.d.ts +95 -0
  262. package/dist/types/schemas/notifications.js +56 -0
  263. package/dist/types/schemas/notifications.js.map +1 -0
  264. package/dist/types/schemas/organizations.d.ts +8 -0
  265. package/dist/types/schemas/organizations.js +2 -0
  266. package/dist/types/schemas/organizations.js.map +1 -1
  267. package/dist/types/schemas/progress.d.ts +3 -3
  268. package/dist/types/schemas/shared.d.ts +3 -1
  269. package/dist/types/schemas/shared.js +5 -1
  270. package/dist/types/schemas/shared.js.map +1 -1
  271. package/package.json +2 -2
  272. package/dist/reporting/courses/index.d.ts +0 -4
  273. package/dist/reporting/courses/index.js +0 -2
  274. package/dist/reporting/courses/model.d.ts +0 -24
  275. package/dist/reporting/courses/ports.d.ts +0 -13
  276. package/dist/reporting/courses/service.d.ts +0 -19
  277. package/dist/reporting/courses/service.js +0 -20
  278. package/dist/reporting/courses/service.js.map +0 -1
  279. package/dist/reporting/dashboard/index.d.ts +0 -4
  280. package/dist/reporting/dashboard/index.js +0 -2
  281. package/dist/reporting/dashboard/model.d.ts +0 -14
  282. package/dist/reporting/dashboard/ports.d.ts +0 -13
  283. package/dist/reporting/dashboard/service.d.ts +0 -19
  284. package/dist/reporting/dashboard/service.js +0 -20
  285. package/dist/reporting/dashboard/service.js.map +0 -1
  286. package/dist/reporting/learn/index.d.ts +0 -5
  287. package/dist/reporting/learn/index.js +0 -2
  288. package/dist/reporting/learn/model.d.ts +0 -23
  289. package/dist/reporting/learn/ports.d.ts +0 -43
  290. package/dist/reporting/learn/service.d.ts +0 -49
  291. package/dist/reporting/learn/service.js +0 -116
  292. package/dist/reporting/learn/service.js.map +0 -1
  293. package/dist/reporting/students/index.d.ts +0 -4
  294. package/dist/reporting/students/index.js +0 -2
  295. package/dist/reporting/students/model.d.ts +0 -51
  296. package/dist/reporting/students/model.js +0 -1
  297. package/dist/reporting/students/ports.d.ts +0 -15
  298. package/dist/reporting/students/ports.js +0 -1
  299. package/dist/reporting/students/service.d.ts +0 -20
  300. package/dist/reporting/students/service.js +0 -23
  301. package/dist/reporting/students/service.js.map +0 -1
  302. /package/dist/reporting/{courses → admin}/model.js +0 -0
  303. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  304. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  305. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  306. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
  307. /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
@@ -0,0 +1,364 @@
1
+ import { isStaffRole } from "../../organizations/roles.js";
2
+ import { channelOrder } from "../../shared/notification-categories.js";
3
+ import { seesSpace } from "../../entitlements/rules.js";
4
+ import { resolveChannels } from "../../shared/notification-settings.js";
5
+ //#region src/reporting/member/service.ts
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
+ };
21
+ }
22
+ function isActivityPublished(settings) {
23
+ return settings?.published !== false;
24
+ }
25
+ var MemberReportServiceImpl = class {
26
+ repo;
27
+ content;
28
+ progress;
29
+ files;
30
+ fileUrls;
31
+ access;
32
+ notificationList;
33
+ titles;
34
+ subscriptions;
35
+ notificationCategories;
36
+ notificationSettings;
37
+ channels;
38
+ constructor(params) {
39
+ this.repo = params.repo;
40
+ this.content = params.content;
41
+ this.progress = params.progress;
42
+ this.files = params.files;
43
+ this.fileUrls = params.fileUrls;
44
+ this.access = params.access;
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
+ });
99
+ }
100
+ async notifications(orgId, actor, userId, filter, page) {
101
+ const listed = await this.notificationList.list(orgId, userId, filter, page);
102
+ return {
103
+ rows: await this.describeNotifications(orgId, actor, listed.items),
104
+ total: listed.total,
105
+ page: page.page,
106
+ pageSize: page.pageSize,
107
+ unreadCount: listed.unreadCount
108
+ };
109
+ }
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);
119
+ }
120
+ async describeNotifications(orgId, actor, rows) {
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)
138
+ ]);
139
+ const titles = {
140
+ ...contentTitles,
141
+ ...discussionTitles
142
+ };
143
+ const personById = new Map(people.map((person) => [person.orgUserId, person]));
144
+ return rows.map((row) => {
145
+ const person = row.actorOrgUserId ? personById.get(row.actorOrgUserId) : void 0;
146
+ const community = row.communityId ? titles[`community:${row.communityId}`] : void 0;
147
+ const space = row.spaceId ? titles[`space:${row.spaceId}`] : void 0;
148
+ return {
149
+ id: row.id,
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,
164
+ actor: person ? {
165
+ orgUserId: person.orgUserId,
166
+ name: person.displayName,
167
+ imageUrl: person.imageAssetId ? this.fileUrls.personImage(person.orgUserId) : null
168
+ } : null,
169
+ subject: itemView(row.subject, titles),
170
+ parent: itemView(row.parent, titles),
171
+ createdAt: row.createdAt,
172
+ readAt: row.readAt
173
+ };
174
+ });
175
+ }
176
+ async listCourses(orgId, orgUserId) {
177
+ const ids = await this.access.listAccessibleContent(orgId, orgUserId, "course");
178
+ return (await Promise.all(ids.map((id) => this.content.getCourse(orgId, id)))).filter((c) => c !== null && c.status === "published");
179
+ }
180
+ async getCourse(orgId, orgUserId, courseId) {
181
+ if (!await this.access.hasAccess(orgId, orgUserId, courseId)) return null;
182
+ const course = await this.content.getCourse(orgId, courseId);
183
+ return course && course.status === "published" ? course : null;
184
+ }
185
+ async listModules(orgId, orgUserId, courseId) {
186
+ if (!await this.getCourse(orgId, orgUserId, courseId)) return null;
187
+ return this.content.listCourseModules(orgId, courseId);
188
+ }
189
+ async listActivities(orgId, orgUserId, courseId) {
190
+ if (!await this.getCourse(orgId, orgUserId, courseId)) return null;
191
+ return (await this.content.listCourseActivityViews(orgId, courseId)).filter((a) => isActivityPublished(a.settings));
192
+ }
193
+ async courseProgress(orgId, orgUserId, courseId) {
194
+ if (!await this.getCourse(orgId, orgUserId, courseId)) return null;
195
+ const ids = (await this.content.listCourseActivities(orgId, courseId)).filter((a) => isActivityPublished(a.settings)).map((a) => a.id);
196
+ const records = await this.progress.listByTargets(orgId, orgUserId, ids);
197
+ const activities = {};
198
+ const positions = {};
199
+ let done = 0;
200
+ for (const r of records) {
201
+ if (r.targetType !== "activity") continue;
202
+ activities[r.targetId] = r.completedAt ? "completed" : "in-progress";
203
+ if (r.completedAt) done += 1;
204
+ if (r.position != null) positions[r.targetId] = r.position;
205
+ }
206
+ const courseRecord = await this.progress.get(orgId, {
207
+ orgUserId,
208
+ targetType: "course",
209
+ targetId: courseId
210
+ });
211
+ return {
212
+ activities,
213
+ percent: ids.length > 0 ? Math.round(done / ids.length * 100) : 0,
214
+ completed: courseRecord?.completedAt != null,
215
+ positions
216
+ };
217
+ }
218
+ async listDownloads(orgId, orgUserId) {
219
+ const ids = await this.access.listAccessibleContent(orgId, orgUserId, "download");
220
+ return (await Promise.all(ids.map((id) => this.content.getDownload(orgId, id)))).filter((d) => d !== null && d.status === "published");
221
+ }
222
+ async getDownload(orgId, orgUserId, downloadId) {
223
+ if (!await this.access.hasAccess(orgId, orgUserId, downloadId)) return null;
224
+ const download = await this.content.getDownload(orgId, downloadId);
225
+ if (!download || download.status !== "published") return null;
226
+ return {
227
+ download,
228
+ assets: await this.content.listDownloadAssets(orgId, downloadId)
229
+ };
230
+ }
231
+ async thumbnailFile(orgId, actor, owner, id) {
232
+ const item = owner === "course" ? await this.content.getCourse(orgId, id) : owner === "download" ? await this.content.getDownload(orgId, id) : await this.content.getCommunity(orgId, id);
233
+ if (!item?.thumbnailAssetId || item.status !== "published" && !isStaffRole(actor.role)) return null;
234
+ return this.files.signFile(orgId, item.thumbnailAssetId);
235
+ }
236
+ async activityFile(orgId, actor, activityId, assetId) {
237
+ const activity = await this.content.getActivity(orgId, activityId);
238
+ if (!activity) return null;
239
+ if (!isStaffRole(actor.role)) {
240
+ if (!isActivityPublished(activity.settings) || !await this.getCourse(orgId, actor.id, activity.courseId)) return null;
241
+ }
242
+ if (!(await this.content.listCourseActivityAssets(orgId, activity.courseId)).some((link) => link.activityId === activityId && link.assetId === assetId)) return null;
243
+ return this.files.signFile(orgId, assetId);
244
+ }
245
+ async downloadFile(orgId, actor, downloadId, assetId) {
246
+ if (!isStaffRole(actor.role) && !await this.getDownload(orgId, actor.id, downloadId)) return null;
247
+ const file = (await this.content.listDownloadAssets(orgId, downloadId)).find((f) => f.assetId === assetId);
248
+ if (!file) return null;
249
+ return this.files.signFile(orgId, assetId, { saveAs: file.displayName ?? file.filename });
250
+ }
251
+ async personImage(orgId, orgUserId) {
252
+ const [person] = await this.repo.people(orgId, [orgUserId]);
253
+ if (!person?.imageAssetId) return null;
254
+ return this.files.signFile(orgId, person.imageAssetId);
255
+ }
256
+ async directory(orgId, actor, communityId, query) {
257
+ if (!await this.access.isCommunityMember(orgId, actor, communityId)) return null;
258
+ if (!await this.content.getCommunity(orgId, communityId)) return null;
259
+ const page = await this.repo.directory(orgId, communityId, query);
260
+ return {
261
+ ...page,
262
+ rows: page.rows.map((row) => ({
263
+ ...row,
264
+ imageUrl: row.imageAssetId ? this.fileUrls.personImage(row.orgUserId) : null
265
+ }))
266
+ };
267
+ }
268
+ async communityHome(orgId, actor, communityId) {
269
+ if (!await this.access.isCommunityMember(orgId, actor, communityId)) return null;
270
+ const community = await this.content.getCommunity(orgId, communityId);
271
+ if (!community) return null;
272
+ const spaces = await this.content.listSpaces(orgId, communityId);
273
+ return {
274
+ communityId,
275
+ title: community.title,
276
+ description: community.description,
277
+ thumbnailUrl: community.thumbnailUrl,
278
+ dm: community.settings.dm,
279
+ spaces: await this.homeSpaces(orgId, actor, communityId, spaces)
280
+ };
281
+ }
282
+ async communitySpace(orgId, actor, communityId, spaceId) {
283
+ if (!await this.access.isCommunityMember(orgId, actor, communityId)) return null;
284
+ const space = await this.content.getSpace(orgId, spaceId);
285
+ if (!space || space.communityId !== communityId) return null;
286
+ const [view] = await this.homeSpaces(orgId, actor, communityId, [space]);
287
+ return view ?? null;
288
+ }
289
+ async homeSpaces(orgId, actor, communityId, spaces) {
290
+ const [openIds, counts, standings, subscribed] = await Promise.all([
291
+ this.access.openSpaceIds(orgId, actor, communityId),
292
+ this.repo.spaceMemberCounts(orgId, communityId),
293
+ this.access.spaceStandings(orgId, actor, communityId),
294
+ this.subscriptions.viewerSubscriptions(orgId, actor.id, "space", spaces.map((space) => space.id), /* @__PURE__ */ new Date())
295
+ ]);
296
+ const open = new Set(openIds);
297
+ const seen = spaces.filter((space) => seesSpace(actor.role, space.accessRule, standings[space.id]));
298
+ return Promise.all(seen.map(async (space) => {
299
+ const memberCount = counts[space.id] ?? 0;
300
+ const base = {
301
+ id: space.id,
302
+ name: space.name,
303
+ memberCount,
304
+ accessRule: space.accessRule,
305
+ standing: standingOf(space, open.has(space.id), memberCount, standings[space.id])
306
+ };
307
+ if (!open.has(space.id)) return {
308
+ ...base,
309
+ locked: true
310
+ };
311
+ return {
312
+ ...base,
313
+ locked: false,
314
+ description: space.description,
315
+ capacity: space.capacity,
316
+ settings: space.settings,
317
+ topics: space.topics.map((topic) => ({
318
+ id: topic.id,
319
+ name: topic.name
320
+ })),
321
+ links: await this.linkViews(orgId, actor, space.links),
322
+ subscription: subscribed[space.id] ?? null
323
+ };
324
+ }));
325
+ }
326
+ async linkViews(orgId, actor, links) {
327
+ const shown = (status) => status === "published" || isStaffRole(actor.role);
328
+ return (await Promise.all(links.map(async (link) => {
329
+ const course = await this.content.getCourse(orgId, link.contentId);
330
+ if (course) return shown(course.status) ? {
331
+ id: link.id,
332
+ contentId: link.contentId,
333
+ type: "course",
334
+ title: course.title
335
+ } : null;
336
+ const download = await this.content.getDownload(orgId, link.contentId);
337
+ if (download) return shown(download.status) ? {
338
+ id: link.id,
339
+ contentId: link.contentId,
340
+ type: "download",
341
+ title: download.title
342
+ } : null;
343
+ const community = await this.content.getCommunity(orgId, link.contentId);
344
+ if (community) return shown(community.status) ? {
345
+ id: link.id,
346
+ contentId: link.contentId,
347
+ type: "community",
348
+ title: community.title
349
+ } : null;
350
+ return null;
351
+ }))).filter((view) => view !== null);
352
+ }
353
+ };
354
+ function standingOf(space, open, memberCount, status) {
355
+ if (space.accessRule === "all_members" || open && status !== "active") return "open";
356
+ if (status === "active") return "member";
357
+ if (status === "requested" || status === "invited") return status;
358
+ if (space.capacity !== null && memberCount >= space.capacity) return "full";
359
+ return "none";
360
+ }
361
+ //#endregion
362
+ export { MemberReportServiceImpl };
363
+
364
+ //# sourceMappingURL=service.js.map
@@ -0,0 +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 {\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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"}
@@ -0,0 +1,9 @@
1
+ import { Role } from "../types/schemas/organizations.js";
2
+ //#region src/shared/actor.d.ts
3
+ interface Actor {
4
+ id: string;
5
+ role: Role;
6
+ }
7
+ //#endregion
8
+ export { Actor };
9
+ //# sourceMappingURL=actor.d.ts.map
@@ -17,6 +17,24 @@ declare class ConflictError extends Error {
17
17
  declare class ForbiddenError extends Error {
18
18
  constructor(message: string);
19
19
  }
20
+ /** The caller's own input breaks a rule, independent of existing state
21
+ * (wrong file type, too large, talking to yourself, …). The HTTP layer maps
22
+ * this to 400. */
23
+ declare class InvalidInputError extends Error {
24
+ constructor(message: string);
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
+ }
20
38
  //#endregion
21
- export { ConflictError, ForbiddenError, NotFoundError };
39
+ export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError, ValidationProblem };
22
40
  //# sourceMappingURL=errors.d.ts.map
@@ -28,7 +28,33 @@ var ForbiddenError = class extends Error {
28
28
  this.name = "ForbiddenError";
29
29
  }
30
30
  };
31
+ /** The caller's own input breaks a rule, independent of existing state
32
+ * (wrong file type, too large, talking to yourself, …). The HTTP layer maps
33
+ * this to 400. */
34
+ var InvalidInputError = class extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "InvalidInputError";
38
+ }
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
+ };
31
57
  //#endregion
32
- export { ConflictError, ForbiddenError, NotFoundError };
58
+ export { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError };
33
59
 
34
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"],"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"}
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"}
@@ -30,6 +30,20 @@ declare const ID_PREFIXES: {
30
30
  readonly automationRun: "run";
31
31
  readonly comment: "cmt";
32
32
  readonly commentReport: "crp";
33
+ readonly community: "cmy";
34
+ readonly space: "spc";
35
+ readonly topic: "top";
36
+ readonly spaceLink: "slk";
37
+ readonly spaceMember: "spm";
38
+ readonly post: "pst";
39
+ readonly postFlag: "pfl";
40
+ readonly chat: "cht";
41
+ readonly message: "msg";
42
+ readonly messageFlag: "mfl";
43
+ readonly notification: "ntf";
44
+ readonly notificationSetting: "nst";
45
+ readonly notificationDelivery: "ndl";
46
+ readonly device: "dev";
33
47
  };
34
48
  type IdType = keyof typeof ID_PREFIXES;
35
49
  /** A bare KSUID body (27-char base62, time-sortable), no prefix. */
package/dist/shared/id.js CHANGED
@@ -30,7 +30,21 @@ const ID_PREFIXES = {
30
30
  automation: "atm",
31
31
  automationRun: "run",
32
32
  comment: "cmt",
33
- commentReport: "crp"
33
+ commentReport: "crp",
34
+ community: "cmy",
35
+ space: "spc",
36
+ topic: "top",
37
+ spaceLink: "slk",
38
+ spaceMember: "spm",
39
+ post: "pst",
40
+ postFlag: "pfl",
41
+ chat: "cht",
42
+ message: "msg",
43
+ messageFlag: "mfl",
44
+ notification: "ntf",
45
+ notificationSetting: "nst",
46
+ notificationDelivery: "ndl",
47
+ device: "dev"
34
48
  };
35
49
  /** A bare KSUID body (27-char base62, time-sortable), no prefix. */
36
50
  function ksuid() {
@@ -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} 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;AACjB;;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"}
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"}
@@ -6,13 +6,26 @@ import "./ports.js";
6
6
  /** Resolves the recipient/content details a template needs when the
7
7
  * triggering event carries only row ids. Wired by the app layer. */
8
8
  interface MailerLookups {
9
- orgUserEmail(orgId: string, orgUserId: string): Promise<string | null>;
9
+ orgUserPerson(orgId: string, orgUserId: string): Promise<{
10
+ userId: string;
11
+ email: string;
12
+ } | null>;
10
13
  contentInfo(orgId: string, contentId: string): Promise<{
11
14
  id: string;
12
15
  title: string;
13
16
  type: ContentType;
14
17
  } | null>;
15
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;
16
29
  declare class Mailer {
17
30
  private readonly templates;
18
31
  private readonly email;
@@ -21,5 +34,5 @@ declare class Mailer {
21
34
  send<K extends EmailTemplateId>(to: string, id: K, params: EmailTemplateParams[K], ctx?: Partial<TemplateContext>): Promise<void>;
22
35
  }
23
36
  //#endregion
24
- export { Mailer, MailerLookups };
37
+ export { EmailRejectedError, EmailRenderError, Mailer, MailerLookups, isEmailRejected, isEmailRenderError };
25
38
  //# sourceMappingURL=mailer.d.ts.map