@headless-lms/core 0.1.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/dist/assets/events.d.ts +81 -0
  3. package/dist/assets/events.js +24 -0
  4. package/dist/assets/events.js.map +1 -0
  5. package/dist/assets/index.d.ts +7 -0
  6. package/dist/assets/index.js +3 -0
  7. package/dist/assets/model.d.ts +4 -0
  8. package/dist/assets/model.js +1 -0
  9. package/dist/assets/ports.d.ts +34 -0
  10. package/dist/assets/ports.js +1 -0
  11. package/dist/assets/service.d.ts +29 -0
  12. package/dist/assets/service.js +130 -0
  13. package/dist/assets/service.js.map +1 -0
  14. package/dist/automations/actions.d.ts +24 -0
  15. package/dist/automations/actions.js +59 -0
  16. package/dist/automations/actions.js.map +1 -0
  17. package/dist/automations/catalog.d.ts +12 -0
  18. package/dist/automations/catalog.js +224 -0
  19. package/dist/automations/catalog.js.map +1 -0
  20. package/dist/automations/events.d.ts +264 -0
  21. package/dist/automations/events.js +54 -0
  22. package/dist/automations/events.js.map +1 -0
  23. package/dist/automations/index.d.ts +9 -0
  24. package/dist/automations/index.js +4 -0
  25. package/dist/automations/model.d.ts +12 -0
  26. package/dist/automations/model.js +14 -0
  27. package/dist/automations/model.js.map +1 -0
  28. package/dist/automations/ports.d.ts +56 -0
  29. package/dist/automations/ports.js +1 -0
  30. package/dist/automations/service.d.ts +53 -0
  31. package/dist/automations/service.js +225 -0
  32. package/dist/automations/service.js.map +1 -0
  33. package/dist/automations/types.d.ts +3 -0
  34. package/dist/automations/types.js +1 -0
  35. package/dist/content/events.d.ts +410 -0
  36. package/dist/content/events.js +105 -0
  37. package/dist/content/events.js.map +1 -0
  38. package/dist/content/index.d.ts +8 -0
  39. package/dist/content/index.js +3 -0
  40. package/dist/content/model.d.ts +3 -0
  41. package/dist/content/model.js +1 -0
  42. package/dist/content/ports.d.ts +85 -0
  43. package/dist/content/ports.js +1 -0
  44. package/dist/content/service.d.ts +54 -0
  45. package/dist/content/service.js +479 -0
  46. package/dist/content/service.js.map +1 -0
  47. package/dist/content/types.d.ts +4 -0
  48. package/dist/content/types.js +1 -0
  49. package/dist/discussion/events.d.ts +101 -0
  50. package/dist/discussion/events.js +29 -0
  51. package/dist/discussion/events.js.map +1 -0
  52. package/dist/discussion/index.d.ts +8 -0
  53. package/dist/discussion/index.js +3 -0
  54. package/dist/discussion/model.d.ts +5 -0
  55. package/dist/discussion/model.js +1 -0
  56. package/dist/discussion/ports.d.ts +112 -0
  57. package/dist/discussion/ports.js +1 -0
  58. package/dist/discussion/service.d.ts +67 -0
  59. package/dist/discussion/service.js +343 -0
  60. package/dist/discussion/service.js.map +1 -0
  61. package/dist/discussion/types.d.ts +4 -0
  62. package/dist/discussion/types.js +1 -0
  63. package/dist/entitlements/events.d.ts +89 -0
  64. package/dist/entitlements/events.js +29 -0
  65. package/dist/entitlements/events.js.map +1 -0
  66. package/dist/entitlements/index.d.ts +7 -0
  67. package/dist/entitlements/index.js +3 -0
  68. package/dist/entitlements/model.d.ts +4 -0
  69. package/dist/entitlements/model.js +1 -0
  70. package/dist/entitlements/ports.d.ts +34 -0
  71. package/dist/entitlements/ports.js +1 -0
  72. package/dist/entitlements/service.d.ts +27 -0
  73. package/dist/entitlements/service.js +60 -0
  74. package/dist/entitlements/service.js.map +1 -0
  75. package/dist/identity/events.d.ts +63 -0
  76. package/dist/identity/events.js +24 -0
  77. package/dist/identity/events.js.map +1 -0
  78. package/dist/identity/index.d.ts +8 -0
  79. package/dist/identity/index.js +3 -0
  80. package/dist/identity/model.d.ts +3 -0
  81. package/dist/identity/model.js +1 -0
  82. package/dist/identity/ports.d.ts +58 -0
  83. package/dist/identity/ports.js +1 -0
  84. package/dist/identity/service.d.ts +38 -0
  85. package/dist/identity/service.js +79 -0
  86. package/dist/identity/service.js.map +1 -0
  87. package/dist/identity/types.d.ts +4 -0
  88. package/dist/identity/types.js +1 -0
  89. package/dist/integrations/events.d.ts +70 -0
  90. package/dist/integrations/events.js +24 -0
  91. package/dist/integrations/events.js.map +1 -0
  92. package/dist/integrations/index.d.ts +9 -0
  93. package/dist/integrations/index.js +6 -0
  94. package/dist/integrations/model.d.ts +28 -0
  95. package/dist/integrations/model.js +45 -0
  96. package/dist/integrations/model.js.map +1 -0
  97. package/dist/integrations/ports.d.ts +62 -0
  98. package/dist/integrations/ports.js +1 -0
  99. package/dist/integrations/registry.d.ts +7 -0
  100. package/dist/integrations/registry.js +16 -0
  101. package/dist/integrations/registry.js.map +1 -0
  102. package/dist/integrations/service.d.ts +47 -0
  103. package/dist/integrations/service.js +184 -0
  104. package/dist/integrations/service.js.map +1 -0
  105. package/dist/integrations/zod.d.ts +29 -0
  106. package/dist/integrations/zod.js +40 -0
  107. package/dist/integrations/zod.js.map +1 -0
  108. package/dist/organizations/events.d.ts +197 -0
  109. package/dist/organizations/events.js +59 -0
  110. package/dist/organizations/events.js.map +1 -0
  111. package/dist/organizations/index.d.ts +11 -0
  112. package/dist/organizations/index.js +5 -0
  113. package/dist/organizations/members.d.ts +32 -0
  114. package/dist/organizations/members.js +12 -0
  115. package/dist/organizations/members.js.map +1 -0
  116. package/dist/organizations/model.d.ts +3 -0
  117. package/dist/organizations/model.js +1 -0
  118. package/dist/organizations/ports.d.ts +127 -0
  119. package/dist/organizations/ports.js +1 -0
  120. package/dist/organizations/roles.d.ts +23 -0
  121. package/dist/organizations/roles.js +70 -0
  122. package/dist/organizations/roles.js.map +1 -0
  123. package/dist/organizations/service.d.ts +58 -0
  124. package/dist/organizations/service.js +359 -0
  125. package/dist/organizations/service.js.map +1 -0
  126. package/dist/organizations/types.d.ts +39 -0
  127. package/dist/organizations/types.js +1 -0
  128. package/dist/progress/events.d.ts +55 -0
  129. package/dist/progress/events.js +19 -0
  130. package/dist/progress/events.js.map +1 -0
  131. package/dist/progress/index.d.ts +8 -0
  132. package/dist/progress/index.js +3 -0
  133. package/dist/progress/model.d.ts +3 -0
  134. package/dist/progress/model.js +1 -0
  135. package/dist/progress/ports.d.ts +37 -0
  136. package/dist/progress/ports.js +1 -0
  137. package/dist/progress/service.d.ts +33 -0
  138. package/dist/progress/service.js +181 -0
  139. package/dist/progress/service.js.map +1 -0
  140. package/dist/progress/types.d.ts +4 -0
  141. package/dist/progress/types.js +1 -0
  142. package/dist/reporting/dashboard/index.d.ts +4 -0
  143. package/dist/reporting/dashboard/index.js +2 -0
  144. package/dist/reporting/dashboard/model.d.ts +14 -0
  145. package/dist/reporting/dashboard/model.js +1 -0
  146. package/dist/reporting/dashboard/ports.d.ts +13 -0
  147. package/dist/reporting/dashboard/ports.js +1 -0
  148. package/dist/reporting/dashboard/service.d.ts +19 -0
  149. package/dist/reporting/dashboard/service.js +20 -0
  150. package/dist/reporting/dashboard/service.js.map +1 -0
  151. package/dist/reporting/learn/index.d.ts +5 -0
  152. package/dist/reporting/learn/index.js +2 -0
  153. package/dist/reporting/learn/model.d.ts +23 -0
  154. package/dist/reporting/learn/model.js +1 -0
  155. package/dist/reporting/learn/ports.d.ts +42 -0
  156. package/dist/reporting/learn/ports.js +1 -0
  157. package/dist/reporting/learn/service.d.ts +48 -0
  158. package/dist/reporting/learn/service.js +111 -0
  159. package/dist/reporting/learn/service.js.map +1 -0
  160. package/dist/reporting/students/index.d.ts +4 -0
  161. package/dist/reporting/students/index.js +2 -0
  162. package/dist/reporting/students/model.d.ts +31 -0
  163. package/dist/reporting/students/model.js +1 -0
  164. package/dist/reporting/students/ports.d.ts +13 -0
  165. package/dist/reporting/students/ports.js +1 -0
  166. package/dist/reporting/students/service.d.ts +19 -0
  167. package/dist/reporting/students/service.js +20 -0
  168. package/dist/reporting/students/service.js.map +1 -0
  169. package/dist/shared/errors.d.ts +22 -0
  170. package/dist/shared/errors.js +34 -0
  171. package/dist/shared/errors.js.map +1 -0
  172. package/dist/shared/events.d.ts +31 -0
  173. package/dist/shared/events.js +75 -0
  174. package/dist/shared/events.js.map +1 -0
  175. package/dist/shared/id.d.ts +44 -0
  176. package/dist/shared/id.js +53 -0
  177. package/dist/shared/id.js.map +1 -0
  178. package/dist/shared/invite-token.d.ts +9 -0
  179. package/dist/shared/invite-token.js +16 -0
  180. package/dist/shared/invite-token.js.map +1 -0
  181. package/dist/shared/logger.d.ts +19 -0
  182. package/dist/shared/logger.js +44 -0
  183. package/dist/shared/logger.js.map +1 -0
  184. package/dist/shared/mailer.d.ts +23 -0
  185. package/dist/shared/mailer.js +27 -0
  186. package/dist/shared/mailer.js.map +1 -0
  187. package/dist/shared/ports.d.ts +62 -0
  188. package/dist/shared/ports.js +2 -0
  189. package/dist/shared/settings.d.ts +25 -0
  190. package/dist/shared/settings.js +21 -0
  191. package/dist/shared/settings.js.map +1 -0
  192. package/dist/types/assets.d.ts +2 -0
  193. package/dist/types/assets.js +1 -0
  194. package/dist/types/automations.d.ts +23 -0
  195. package/dist/types/automations.js +1 -0
  196. package/dist/types/content.d.ts +2 -0
  197. package/dist/types/content.js +1 -0
  198. package/dist/types/discussion.d.ts +36 -0
  199. package/dist/types/discussion.js +1 -0
  200. package/dist/types/email-templates.d.ts +57 -0
  201. package/dist/types/email-templates.js +1 -0
  202. package/dist/types/entitlements.d.ts +3 -0
  203. package/dist/types/entitlements.js +1 -0
  204. package/dist/types/events.d.ts +25 -0
  205. package/dist/types/events.js +1 -0
  206. package/dist/types/identity.d.ts +37 -0
  207. package/dist/types/identity.js +1 -0
  208. package/dist/types/index.d.ts +23 -0
  209. package/dist/types/index.js +1 -0
  210. package/dist/types/integrations.d.ts +45 -0
  211. package/dist/types/integrations.js +1 -0
  212. package/dist/types/organizations.d.ts +15 -0
  213. package/dist/types/organizations.js +1 -0
  214. package/dist/types/ports.d.ts +57 -0
  215. package/dist/types/ports.js +1 -0
  216. package/dist/types/progress.d.ts +6 -0
  217. package/dist/types/progress.js +1 -0
  218. package/dist/types/schemas/assets.d.ts +109 -0
  219. package/dist/types/schemas/assets.js +49 -0
  220. package/dist/types/schemas/assets.js.map +1 -0
  221. package/dist/types/schemas/automations.d.ts +118 -0
  222. package/dist/types/schemas/automations.js +73 -0
  223. package/dist/types/schemas/automations.js.map +1 -0
  224. package/dist/types/schemas/content.d.ts +216 -0
  225. package/dist/types/schemas/content.js +138 -0
  226. package/dist/types/schemas/content.js.map +1 -0
  227. package/dist/types/schemas/credentials.d.ts +15 -0
  228. package/dist/types/schemas/credentials.js +15 -0
  229. package/dist/types/schemas/credentials.js.map +1 -0
  230. package/dist/types/schemas/discussion.d.ts +192 -0
  231. package/dist/types/schemas/discussion.js +100 -0
  232. package/dist/types/schemas/discussion.js.map +1 -0
  233. package/dist/types/schemas/entitlements.d.ts +50 -0
  234. package/dist/types/schemas/entitlements.js +38 -0
  235. package/dist/types/schemas/entitlements.js.map +1 -0
  236. package/dist/types/schemas/identity.d.ts +37 -0
  237. package/dist/types/schemas/identity.js +34 -0
  238. package/dist/types/schemas/identity.js.map +1 -0
  239. package/dist/types/schemas/index.d.ts +14 -0
  240. package/dist/types/schemas/index.js +14 -0
  241. package/dist/types/schemas/integrations.d.ts +36 -0
  242. package/dist/types/schemas/integrations.js +30 -0
  243. package/dist/types/schemas/integrations.js.map +1 -0
  244. package/dist/types/schemas/organizations.d.ts +135 -0
  245. package/dist/types/schemas/organizations.js +90 -0
  246. package/dist/types/schemas/organizations.js.map +1 -0
  247. package/dist/types/schemas/outbox.d.ts +19 -0
  248. package/dist/types/schemas/outbox.js +18 -0
  249. package/dist/types/schemas/outbox.js.map +1 -0
  250. package/dist/types/schemas/progress.d.ts +53 -0
  251. package/dist/types/schemas/progress.js +37 -0
  252. package/dist/types/schemas/progress.js.map +1 -0
  253. package/dist/types/schemas/settings.d.ts +16 -0
  254. package/dist/types/schemas/settings.js +15 -0
  255. package/dist/types/schemas/settings.js.map +1 -0
  256. package/dist/types/schemas/shared.d.ts +13 -0
  257. package/dist/types/schemas/shared.js +28 -0
  258. package/dist/types/schemas/shared.js.map +1 -0
  259. package/dist/types/shared.d.ts +25 -0
  260. package/dist/types/shared.js +1 -0
  261. package/package.json +61 -0
@@ -0,0 +1,359 @@
1
+ import { genId } from "../shared/id.js";
2
+ import { noopLogger } from "../shared/logger.js";
3
+ import { NotFoundError } from "../shared/errors.js";
4
+ import { STUDENT_ROLE } from "./roles.js";
5
+ import { InviteError, OrganizationRuleError } from "./members.js";
6
+ import { organizationEvents } from "./events.js";
7
+ import { generateInviteToken, hashInviteToken } from "../shared/invite-token.js";
8
+ //#region src/organizations/service.ts
9
+ const INVITE_TTL_MS = 10080 * 60 * 1e3;
10
+ function toMember(r) {
11
+ return {
12
+ id: r.id,
13
+ firstName: r.firstName,
14
+ lastName: r.lastName,
15
+ email: r.email,
16
+ image: r.image,
17
+ role: r.role,
18
+ status: r.status,
19
+ joinedAt: r.joinedAt,
20
+ invitedAt: r.invitedAt
21
+ };
22
+ }
23
+ var OrganizationServiceImpl = class {
24
+ repo;
25
+ membersRepo;
26
+ people;
27
+ uow;
28
+ logger;
29
+ mailer;
30
+ inviteUrls;
31
+ constructor(params) {
32
+ this.repo = params.repo;
33
+ this.membersRepo = params.membersRepo;
34
+ this.people = params.people;
35
+ this.uow = params.uow;
36
+ this.logger = params.logger ?? noopLogger;
37
+ this.mailer = params.mailer;
38
+ this.inviteUrls = params.inviteUrls;
39
+ }
40
+ async createOrganization(input) {
41
+ const created = await this.uow.run(async ({ organizations, outbox }) => {
42
+ const id = genId("organization");
43
+ const created = await organizations.create({
44
+ id,
45
+ externalId: input.externalId ?? id,
46
+ name: input.name ?? `My Org`,
47
+ slug: input.slug ?? "",
48
+ ownerId: input.ownerId
49
+ });
50
+ await outbox.append([organizationEvents.organizationCreated.make({
51
+ orgId: created.id,
52
+ data: created
53
+ })]);
54
+ return created;
55
+ });
56
+ this.logger.info("organization created", {
57
+ orgId: created.id,
58
+ slug: created.slug
59
+ });
60
+ return created;
61
+ }
62
+ async updateOrganization(id, input) {
63
+ const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {
64
+ const updated = await organizations.update(id, {
65
+ name: input.name,
66
+ slug: input.slug
67
+ });
68
+ if (!updated) throw new NotFoundError("Organization", id);
69
+ await outbox.append([organizationEvents.organizationUpdated.make({
70
+ orgId: id,
71
+ data: updated
72
+ })]);
73
+ return updated;
74
+ });
75
+ this.logger.info("organization updated", { updatedOrg });
76
+ return updatedOrg;
77
+ }
78
+ async deleteOrganization(id) {
79
+ const deleted = await this.uow.run(async ({ organizations, outbox }) => {
80
+ const deleted = await organizations.delete(id);
81
+ await outbox.append([organizationEvents.organizationDeleted.make({
82
+ orgId: deleted.id,
83
+ data: deleted
84
+ })]);
85
+ return deleted;
86
+ });
87
+ this.logger.info("organization deleted", { orgId: deleted.id });
88
+ return deleted;
89
+ }
90
+ async addOrgUser({ orgId, userId, role }) {
91
+ const orgUser = await this.uow.run(async ({ organizations, outbox }) => {
92
+ const created = await organizations.createOrgUser({
93
+ orgId,
94
+ role,
95
+ userId,
96
+ status: "active"
97
+ });
98
+ await outbox.append([organizationEvents.orgUserLinked.make({
99
+ orgId,
100
+ data: created
101
+ })]);
102
+ return created;
103
+ });
104
+ this.logger.info("user added to organization", {
105
+ orgId,
106
+ userId,
107
+ role
108
+ });
109
+ return orgUser;
110
+ }
111
+ async removeOrgUser(orgId, id) {
112
+ const deleted = await this.uow.run(async ({ organizations, outbox }) => {
113
+ const deletedUser = await organizations.deleteOrgUser(orgId, id);
114
+ if (!deletedUser) throw new NotFoundError("OrgUser", id);
115
+ await outbox.append([organizationEvents.orgUserDeleted.make({
116
+ orgId,
117
+ data: deletedUser
118
+ })]);
119
+ return deletedUser;
120
+ });
121
+ this.logger.info("organization deleted", { orgId: deleted.id });
122
+ return deleted;
123
+ }
124
+ async createInvite(input) {
125
+ const { orgId, email, role, inviterUserId, firstName, lastName } = input;
126
+ const sendEmail = input.sendEmail ?? true;
127
+ const { token, tokenHash } = generateInviteToken();
128
+ const expiresAt = new Date(Date.now() + INVITE_TTL_MS);
129
+ const person = role === "student" ? await this.people.createUser({
130
+ email,
131
+ ...firstName !== void 0 && { firstName },
132
+ ...lastName !== void 0 && { lastName }
133
+ }) : null;
134
+ const invite = await this.uow.run(async ({ organizations, outbox }) => {
135
+ const row = await organizations.upsertPendingInvite(orgId, {
136
+ email,
137
+ role,
138
+ invitedBy: inviterUserId,
139
+ tokenHash,
140
+ expiresAt
141
+ });
142
+ const events = [organizationEvents.inviteCreated.make({
143
+ orgId,
144
+ data: row
145
+ })];
146
+ if (person) {
147
+ const { orgUser, created } = await organizations.ensureOrgUser({
148
+ orgId,
149
+ userId: person.id,
150
+ role: STUDENT_ROLE,
151
+ status: "invited"
152
+ });
153
+ if (created) events.push(organizationEvents.studentCreated.make({
154
+ orgId,
155
+ data: orgUser
156
+ }));
157
+ }
158
+ await outbox.append(events);
159
+ return row;
160
+ });
161
+ if (sendEmail) await this.sendInviteEmail(invite, token);
162
+ this.logger.info("invite created", {
163
+ orgId,
164
+ inviteId: invite.id,
165
+ role,
166
+ sendEmail
167
+ });
168
+ return invite;
169
+ }
170
+ async updateStudent(input) {
171
+ const { orgId, orgUserId, firstName, lastName, email } = input;
172
+ const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
173
+ if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
174
+ const person = await this.people.getUserById(orgUser.userId);
175
+ if (!person) throw new NotFoundError("User", orgUser.userId);
176
+ const previousEmail = person.email;
177
+ await this.people.updateUser(person.id, {
178
+ ...firstName !== void 0 && { firstName },
179
+ ...lastName !== void 0 && { lastName },
180
+ ...email !== void 0 && { email }
181
+ });
182
+ const emailChanged = email !== void 0 && email.toLowerCase() !== previousEmail.toLowerCase();
183
+ if (emailChanged && orgUser.status !== "active") await this.repo.cancelPendingInvite(orgId, previousEmail);
184
+ this.logger.info("student updated", {
185
+ orgId,
186
+ orgUserId,
187
+ emailChanged
188
+ });
189
+ return orgUser;
190
+ }
191
+ async resendStudentInvite(input) {
192
+ const { orgId, orgUserId, inviterUserId } = input;
193
+ const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);
194
+ if (!orgUser || orgUser.role !== "student") throw new NotFoundError("Student", orgUserId);
195
+ if (orgUser.status === "active") throw new OrganizationRuleError("That student has already accepted their invitation");
196
+ const person = await this.people.getUserById(orgUser.userId);
197
+ if (!person) throw new NotFoundError("User", orgUser.userId);
198
+ await this.createInvite({
199
+ orgId,
200
+ email: person.email,
201
+ role: STUDENT_ROLE,
202
+ inviterUserId,
203
+ sendEmail: true
204
+ });
205
+ this.logger.info("student invite resent", {
206
+ orgId,
207
+ orgUserId
208
+ });
209
+ }
210
+ async peekInvite(token) {
211
+ const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));
212
+ if (!invite || invite.status !== "pending") return null;
213
+ if (invite.expiresAt && invite.expiresAt < /* @__PURE__ */ new Date()) return null;
214
+ return invite;
215
+ }
216
+ async acceptInvite(input) {
217
+ const invite = await this.peekInvite(input.token);
218
+ if (!invite) {
219
+ this.logger.warn("invite accept refused: token invalid or expired");
220
+ throw new InviteError("invalid invite");
221
+ }
222
+ if (invite.email.toLowerCase() !== input.email.toLowerCase()) {
223
+ this.logger.warn("invite accept refused: email mismatch", {
224
+ orgId: invite.orgId,
225
+ inviteId: invite.id
226
+ });
227
+ throw new InviteError("emails must match");
228
+ }
229
+ if (!await this.repo.findById(invite.orgId)) throw new InviteError("Org not found");
230
+ const orgUser = await this.uow.run(async ({ organizations, outbox }) => {
231
+ const { orgUser: row, created } = await organizations.ensureOrgUser({
232
+ orgId: invite.orgId,
233
+ userId: input.userId,
234
+ role: invite.role,
235
+ status: "active"
236
+ });
237
+ const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, "accepted");
238
+ if (!acceptedInvite) throw new NotFoundError("Invite", invite.id);
239
+ const events = [organizationEvents.inviteAccepted.make({
240
+ orgId: invite.orgId,
241
+ data: acceptedInvite
242
+ })];
243
+ if (row.role === "student") events.push(created ? organizationEvents.studentCreated.make({
244
+ orgId: invite.orgId,
245
+ data: row
246
+ }) : organizationEvents.studentLinked.make({
247
+ orgId: invite.orgId,
248
+ data: row
249
+ }));
250
+ else if (created) events.push(organizationEvents.orgUserLinked.make({
251
+ orgId: invite.orgId,
252
+ data: row
253
+ }));
254
+ await outbox.append(events);
255
+ return row;
256
+ });
257
+ this.logger.info("invite accepted", {
258
+ orgId: invite.orgId,
259
+ userId: input.userId
260
+ });
261
+ return orgUser;
262
+ }
263
+ async getOrgUser(orgId, userId) {
264
+ return this.repo.findOrgUser(orgId, userId);
265
+ }
266
+ async deleteOrgUser(orgId, id) {
267
+ const deleted = await this.uow.run(async ({ organizations, outbox }) => {
268
+ const orgUser = await organizations.findOrgUserById(orgId, id);
269
+ if (!orgUser) throw new NotFoundError("OrgUser", id);
270
+ const deletedUser = await organizations.deleteOrgUser(orgId, id);
271
+ if (!deletedUser) throw new NotFoundError("OrgUser", id);
272
+ const events = [];
273
+ if (deletedUser.role === "student") events.push(organizationEvents.studentDeleted.make({
274
+ orgId,
275
+ data: orgUser
276
+ }));
277
+ await outbox.append(events);
278
+ return deletedUser;
279
+ });
280
+ this.logger.info("org user deleted", {
281
+ orgId,
282
+ orgUserId: id
283
+ });
284
+ return deleted;
285
+ }
286
+ async getOrgUsersForUser(userId) {
287
+ return this.repo.findOrgUsersByUser(userId);
288
+ }
289
+ async getById(id) {
290
+ return this.repo.findById(id);
291
+ }
292
+ async sendInviteEmail(invite, token) {
293
+ if (!this.mailer || !this.inviteUrls) throw new Error("invite delivery is not configured (mailer / invite urls missing)");
294
+ const { email, role } = invite;
295
+ const isStudent = role === STUDENT_ROLE;
296
+ const inviteUrl = `${isStudent ? `${this.inviteUrls.studentPortalUrl}/welcome` : `${this.inviteUrls.adminAppUrl}/invite`}?${(isStudent ? new URLSearchParams({ token }) : new URLSearchParams({
297
+ token,
298
+ email
299
+ })).toString()}`;
300
+ try {
301
+ if (role === "student") await this.mailer.send(email, "studentInvite", {
302
+ inviteUrl,
303
+ studentName: email
304
+ });
305
+ else await this.mailer.send(email, "memberInvite", {
306
+ inviteUrl,
307
+ inviterName: "Your team",
308
+ role
309
+ });
310
+ } catch (err) {
311
+ this.logger.error("failed to send invite email", {
312
+ email,
313
+ role,
314
+ err: err instanceof Error ? err : new Error(String(err))
315
+ });
316
+ }
317
+ }
318
+ async getByExternalId(externalId) {
319
+ return this.repo.findByExternalId(externalId);
320
+ }
321
+ async getBySlug(slug) {
322
+ return this.repo.findBySlug(slug);
323
+ }
324
+ listMembers(orgId, query) {
325
+ return this.membersRepo.list(orgId, query);
326
+ }
327
+ async updateMemberRole(ctx, id, role) {
328
+ const member = await this.membersRepo.findById(ctx.orgId, id);
329
+ if (!member) return null;
330
+ if (member.role === "owner") {
331
+ this.logger.warn("role change rejected: owner role immutable", {
332
+ orgId: ctx.orgId,
333
+ memberId: id
334
+ });
335
+ throw new OrganizationRuleError("The owner role cannot be reassigned");
336
+ }
337
+ if (member.kind !== "member" || !member.userExternalId) {
338
+ this.logger.warn("role change rejected: not an active member", {
339
+ orgId: ctx.orgId,
340
+ memberId: id
341
+ });
342
+ throw new OrganizationRuleError("Only active members can have their role changed");
343
+ }
344
+ const updated = await this.membersRepo.findById(ctx.orgId, id);
345
+ this.logger.info("member role updated", {
346
+ orgId: ctx.orgId,
347
+ memberId: id,
348
+ role
349
+ });
350
+ return updated ? toMember(updated) : null;
351
+ }
352
+ async removeMember(_ctx, _id) {
353
+ return true;
354
+ }
355
+ };
356
+ //#endregion
357
+ export { OrganizationServiceImpl };
358
+
359
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/organizations/service.ts"],"sourcesContent":["// organizations context — service implementation (inbound port).\nimport type {\n MemberRecord,\n MembersRepository,\n MemberWriteContext,\n OrganizationService,\n OrganizationsRepository,\n OrganizationsUnitOfWork,\n} from './ports.js';\nimport type { Invite, Organization, OrgUser } from './model.js';\nimport { type Role, STUDENT_ROLE } from './roles.js';\nimport {\n InviteError,\n type Member,\n type MembersQuery,\n OrganizationRuleError,\n type Page,\n} from './members.js';\nimport type {\n AcceptInviteInput,\n AddOrgUserInput,\n CreateInviteInput,\n CreateOrganizationInput,\n ResendStudentInviteInput,\n UpdateOrganizationInput,\n UpdateStudentInput,\n} from './types.js';\nimport { organizationEvents, type OrganizationEvent } from './events.js';\nimport type { Logger, NewDomainEvent } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer } from '../shared/mailer.js';\nimport { generateInviteToken, hashInviteToken } from '../shared/invite-token.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { IdentityService } from '../identity/index.js';\nimport { genId } from '../shared/id.js';\n\nconst INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\ntype NewOrganizationEvent = NewDomainEvent<OrganizationEvent>;\n\nexport interface InviteUrls {\n studentPortalUrl: string;\n adminAppUrl: string;\n}\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport type OrganizationServiceParams = {\n repo: OrganizationsRepository;\n membersRepo: MembersRepository;\n people: IdentityService;\n uow: OrganizationsUnitOfWork;\n logger?: Logger;\n mailer?: Pick<Mailer, 'send'>;\n inviteUrls?: InviteUrls;\n};\n\nexport class OrganizationServiceImpl implements OrganizationService {\n private readonly repo: OrganizationsRepository;\n private readonly membersRepo: MembersRepository;\n private readonly people: IdentityService;\n private readonly uow: OrganizationsUnitOfWork;\n private readonly logger: Logger;\n private readonly mailer?: Pick<Mailer, 'send'>;\n private readonly inviteUrls?: InviteUrls;\n\n constructor(params: OrganizationServiceParams) {\n this.repo = params.repo;\n this.membersRepo = params.membersRepo;\n this.people = params.people;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n this.mailer = params.mailer;\n this.inviteUrls = params.inviteUrls;\n }\n\n async createOrganization(input: CreateOrganizationInput): Promise<Organization> {\n const created = await this.uow.run(async ({ organizations, outbox }) => {\n const id = genId('organization');\n\n const created = await organizations.create({\n id,\n // The auth provider adopts this id for its own org row, so the org is\n // its own external id — a lookup by either id lands on the same row.\n externalId: input.externalId ?? id,\n name: input.name ?? `My Org`,\n slug: input.slug ?? '',\n ownerId: input.ownerId,\n });\n await outbox.append([\n organizationEvents.organizationCreated.make({\n orgId: created.id,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('organization created', { orgId: created.id, slug: created.slug });\n return created;\n }\n\n async updateOrganization(id: string, input: UpdateOrganizationInput): Promise<Organization> {\n const updatedOrg = await this.uow.run(async ({ organizations, outbox }) => {\n const updated = await organizations.update(id, {\n name: input.name,\n slug: input.slug,\n });\n if (!updated) {\n throw new NotFoundError('Organization', id);\n }\n await outbox.append([\n organizationEvents.organizationUpdated.make({ orgId: id, data: updated }),\n ]);\n return updated;\n });\n\n this.logger.info('organization updated', { updatedOrg });\n return updatedOrg!;\n }\n\n async deleteOrganization(id: string): Promise<Organization> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deleted = await organizations.delete(id);\n\n await outbox.append([\n organizationEvents.organizationDeleted.make({\n orgId: deleted.id,\n data: deleted,\n }),\n ]);\n return deleted;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async addOrgUser({ orgId, userId, role }: AddOrgUserInput): Promise<OrgUser> {\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n const created = await organizations.createOrgUser({ orgId, role, userId, status: 'active' });\n await outbox.append([\n organizationEvents.orgUserLinked.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('user added to organization', { orgId, userId, role });\n return orgUser;\n }\n\n async removeOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n await outbox.append([\n organizationEvents.orgUserDeleted.make({\n orgId,\n data: deletedUser,\n }),\n ]);\n return deletedUser;\n });\n this.logger.info('organization deleted', { orgId: deleted.id });\n return deleted;\n }\n\n async createInvite(input: CreateInviteInput): Promise<Invite> {\n const { orgId, email, role, inviterUserId, firstName, lastName } = input;\n const sendEmail = input.sendEmail ?? true;\n const { token, tokenHash } = generateInviteToken();\n const expiresAt = new Date(Date.now() + INVITE_TTL_MS);\n\n // A student belongs to the org from the moment an admin adds them, not from\n // the moment they click a link — so the person exists before the invite\n // does. Staff are excluded: their org_users row is mirrored from the auth\n // provider's member record, which does not exist until they join.\n const person =\n role === STUDENT_ROLE\n ? await this.people.createUser({\n email,\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n })\n : null;\n\n const invite = await this.uow.run(async ({ organizations, outbox }) => {\n const row = await organizations.upsertPendingInvite(orgId, {\n email,\n role,\n invitedBy: inviterUserId,\n tokenHash,\n expiresAt,\n });\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteCreated.make({ orgId, data: row }),\n ];\n if (person) {\n // Idempotent: re-inviting an address rotates the token without\n // producing a second org user or a second student.created.\n const { orgUser, created } = await organizations.ensureOrgUser({\n orgId,\n userId: person.id,\n role: STUDENT_ROLE,\n status: 'invited',\n });\n if (created) {\n events.push(\n organizationEvents.studentCreated.make({ orgId, data: orgUser }),\n );\n }\n }\n await outbox.append(events);\n return row;\n });\n\n if (sendEmail) {\n // TODO make this workflow durable\n await this.sendInviteEmail(invite, token);\n }\n\n this.logger.info('invite created', { orgId, inviteId: invite.id, role, sendEmail });\n return invite;\n }\n\n async updateStudent(input: UpdateStudentInput): Promise<OrgUser> {\n const { orgId, orgUserId, firstName, lastName, email } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n\n const previousEmail = person.email;\n await this.people.updateUser(person.id, {\n ...(firstName !== undefined && { firstName }),\n ...(lastName !== undefined && { lastName }),\n ...(email !== undefined && { email }),\n });\n\n // The pending invite carries its own copy of the address and a token minted\n // against it. Once the address is corrected that invite points at the wrong\n // person, so it is cancelled rather than left live — the admin re-sends,\n // which mints a fresh one for the new address. A student who has already\n // accepted has no pending invite to worry about.\n const emailChanged = email !== undefined && email.toLowerCase() !== previousEmail.toLowerCase();\n if (emailChanged && orgUser.status !== 'active') {\n await this.repo.cancelPendingInvite(orgId, previousEmail);\n }\n\n this.logger.info('student updated', { orgId, orgUserId, emailChanged });\n return orgUser;\n }\n\n async resendStudentInvite(input: ResendStudentInviteInput): Promise<void> {\n const { orgId, orgUserId, inviterUserId } = input;\n const orgUser = await this.repo.findOrgUserById(orgId, orgUserId);\n if (!orgUser || orgUser.role !== STUDENT_ROLE) {\n throw new NotFoundError('Student', orgUserId);\n }\n if (orgUser.status === 'active') {\n throw new OrganizationRuleError('That student has already accepted their invitation');\n }\n const person = await this.people.getUserById(orgUser.userId);\n if (!person) {\n throw new NotFoundError('User', orgUser.userId);\n }\n // Straight back through createInvite: it rotates the token on the pending\n // row and finds the existing org user, so a resend is an invite that\n // happens to be the second one.\n await this.createInvite({\n orgId,\n email: person.email,\n role: STUDENT_ROLE,\n inviterUserId,\n sendEmail: true,\n });\n this.logger.info('student invite resent', { orgId, orgUserId });\n }\n\n async peekInvite(token: string): Promise<Invite | null> {\n const invite = await this.repo.findInviteByTokenHash(hashInviteToken(token));\n if (!invite || invite.status !== 'pending') {\n return null;\n }\n if (invite.expiresAt && invite.expiresAt < new Date()) {\n return null;\n }\n return invite;\n }\n\n async acceptInvite(input: AcceptInviteInput): Promise<OrgUser> {\n const invite = await this.peekInvite(input.token);\n if (!invite) {\n this.logger.warn('invite accept refused: token invalid or expired');\n throw new InviteError('invalid invite');\n }\n if (invite.email.toLowerCase() !== input.email.toLowerCase()) {\n this.logger.warn('invite accept refused: email mismatch', {\n orgId: invite.orgId,\n inviteId: invite.id,\n });\n throw new InviteError('emails must match');\n }\n const org = await this.repo.findById(invite.orgId);\n if (!org) {\n throw new InviteError('Org not found');\n }\n\n const orgUser = await this.uow.run(async ({ organizations, outbox }) => {\n // A student provisioned at invite time already has a row — acceptance\n // activates it. Staff have none yet, so this inserts one.\n const { orgUser: row, created } = await organizations.ensureOrgUser({\n orgId: invite.orgId,\n userId: input.userId,\n role: invite.role,\n status: 'active',\n });\n const acceptedInvite = await organizations.setInviteStatus(invite.orgId, invite.id, 'accepted');\n if (!acceptedInvite) {\n throw new NotFoundError('Invite', invite.id);\n }\n const events: NewOrganizationEvent[] = [\n organizationEvents.inviteAccepted.make({\n orgId: invite.orgId,\n data: acceptedInvite,\n }),\n ];\n if (row.role === STUDENT_ROLE) {\n events.push(\n created\n ? organizationEvents.studentCreated.make({\n orgId: invite.orgId,\n data: row,\n })\n : organizationEvents.studentLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n } else if (created) {\n events.push(\n organizationEvents.orgUserLinked.make({\n orgId: invite.orgId,\n data: row,\n }),\n );\n }\n await outbox.append(events);\n return row;\n });\n this.logger.info('invite accepted', { orgId: invite.orgId, userId: input.userId });\n return orgUser;\n }\n\n // By identity USER id — `(org_id, user_id)` is unique. Every caller (scope\n // resolution, the MCP principal, the learn routes) asks \"what is this person\n // in this org\", never \"what is this org_users row\".\n async getOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n return this.repo.findOrgUser(orgId, userId);\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser> {\n const deleted = await this.uow.run(async ({ organizations, outbox }) => {\n const orgUser = await organizations.findOrgUserById(orgId, id);\n if (!orgUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const deletedUser = await organizations.deleteOrgUser(orgId, id);\n if (!deletedUser) {\n throw new NotFoundError('OrgUser', id);\n }\n const events: NewOrganizationEvent[] = [];\n if (deletedUser.role === STUDENT_ROLE) {\n events.push(\n organizationEvents.studentDeleted.make({ orgId, data: orgUser }),\n );\n }\n\n await outbox.append(events);\n return deletedUser;\n });\n this.logger.info('org user deleted', { orgId, orgUserId: id });\n return deleted;\n }\n\n async getOrgUsersForUser(userId: string): Promise<OrgUser[]> {\n return this.repo.findOrgUsersByUser(userId);\n }\n\n async getById(id: string): Promise<Organization | null> {\n return this.repo.findById(id);\n }\n\n private async sendInviteEmail(invite: Invite, token: string): Promise<void> {\n if (!this.mailer || !this.inviteUrls) {\n throw new Error('invite delivery is not configured (mailer / invite urls missing)');\n }\n const { email, role } = invite;\n const isStudent = role === STUDENT_ROLE;\n const base = isStudent\n ? `${this.inviteUrls.studentPortalUrl}/welcome`\n : `${this.inviteUrls.adminAppUrl}/invite`;\n // The portal resolves the token to the invited email, so the link carries\n // only the token. The admin app still reads it from the query.\n const query = isStudent\n ? new URLSearchParams({ token })\n : new URLSearchParams({ token, email });\n const inviteUrl = `${base}?${query.toString()}`;\n try {\n if (role === STUDENT_ROLE) {\n await this.mailer.send(email, 'studentInvite', { inviteUrl, studentName: email });\n } else {\n await this.mailer.send(email, 'memberInvite', {\n inviteUrl,\n inviterName: 'Your team',\n role,\n });\n }\n } catch (err) {\n // A failed email must not abort invite creation: the token is already\n // minted and recorded, so the admin can fix transport and resend.\n this.logger.error('failed to send invite email', {\n email,\n role,\n err: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }\n\n async getByExternalId(externalId: string): Promise<Organization | null> {\n return this.repo.findByExternalId(externalId);\n }\n\n async getBySlug(slug: string): Promise<Organization | null> {\n return this.repo.findBySlug(slug);\n }\n\n listMembers(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n return this.membersRepo.list(orgId, query);\n }\n\n async updateMemberRole(ctx: MemberWriteContext, id: string, role: Role): Promise<Member | null> {\n const member = await this.membersRepo.findById(ctx.orgId, id);\n if (!member) {\n return null;\n }\n if (member.role === 'owner') {\n this.logger.warn('role change rejected: owner role immutable', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('The owner role cannot be reassigned');\n }\n if (member.kind !== 'member' || !member.userExternalId) {\n this.logger.warn('role change rejected: not an active member', {\n orgId: ctx.orgId,\n memberId: id,\n });\n throw new OrganizationRuleError('Only active members can have their role changed');\n }\n\n const updated = await this.membersRepo.findById(ctx.orgId, id);\n this.logger.info('member role updated', { orgId: ctx.orgId, memberId: id, role });\n return updated ? toMember(updated) : null;\n }\n\n async removeMember(_ctx: MemberWriteContext, _id: string): Promise<boolean> {\n return true;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { ProvisionUserInput } from "../types/schemas/identity.js";
2
+ import { AcceptInviteInput, AddOrgUserInput, CreateInviteInput, CreateOrgUserInput, InviteRole, NewOrganizationInput, OrgUserStatus, Organization } from "../types/schemas/organizations.js";
3
+ import { InviteId, OrgUserId, OrganizationId } from "../types/organizations.js";
4
+ import "../types/index.js";
5
+ //#region src/organizations/types.d.ts
6
+ interface CreateOrganizationInput {
7
+ externalId?: string;
8
+ name?: string;
9
+ slug?: string;
10
+ logo?: string;
11
+ ownerId: string;
12
+ }
13
+ type UpdateOrganizationInput = Partial<Omit<Organization, 'createdAt,updatedAt'>>;
14
+ interface LinkOrgUserInput {
15
+ orgId: string;
16
+ userId: string;
17
+ role: string;
18
+ }
19
+ interface UnlinkOrgUserInput {
20
+ orgExternalId: string;
21
+ userExternalId: string;
22
+ }
23
+ interface UpdatePersonInput {
24
+ firstName?: string;
25
+ lastName?: string;
26
+ email?: string;
27
+ }
28
+ interface UpdateStudentInput extends UpdatePersonInput {
29
+ orgId: string;
30
+ orgUserId: string;
31
+ }
32
+ interface ResendStudentInviteInput {
33
+ orgId: string;
34
+ orgUserId: string;
35
+ inviterUserId: string;
36
+ }
37
+ //#endregion
38
+ export { type AcceptInviteInput, type AddOrgUserInput, type CreateInviteInput, type CreateOrgUserInput, CreateOrganizationInput, type InviteId, type InviteRole, LinkOrgUserInput, type NewOrganizationInput, type OrgUserId, type OrgUserStatus, type OrganizationId, type ProvisionUserInput, ResendStudentInviteInput, UnlinkOrgUserInput, UpdateOrganizationInput, UpdatePersonInput, UpdateStudentInput };
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import { JsonValue, NewDomainEvent } from "../types/shared.js";
2
+ import { EventOf, EventOfValues, RuntimeEventDefinition, VersionedDomainEvent } from "../shared/events.js";
3
+ import "../shared/ports.js";
4
+ //#region src/progress/events.d.ts
5
+ declare const progressEvents: {
6
+ progressStarted: RuntimeEventDefinition<VersionedDomainEvent<{
7
+ id: string;
8
+ orgId: string;
9
+ orgUserId: string;
10
+ targetType: "course" | "activity" | "module";
11
+ targetId: string;
12
+ startedAt: Date;
13
+ position: JsonValue;
14
+ completedAt: Date | null;
15
+ updatedAt: Date;
16
+ }, "progress.record.started", 1>, {
17
+ id: string;
18
+ orgId: string;
19
+ orgUserId: string;
20
+ targetType: "course" | "activity" | "module";
21
+ targetId: string;
22
+ startedAt: unknown;
23
+ position: unknown;
24
+ completedAt: unknown;
25
+ updatedAt: unknown;
26
+ }>;
27
+ progressCompleted: RuntimeEventDefinition<VersionedDomainEvent<{
28
+ id: string;
29
+ orgId: string;
30
+ orgUserId: string;
31
+ targetType: "course" | "activity" | "module";
32
+ targetId: string;
33
+ startedAt: Date;
34
+ position: JsonValue;
35
+ completedAt: Date | null;
36
+ updatedAt: Date;
37
+ }, "progress.record.completed", 1>, {
38
+ id: string;
39
+ orgId: string;
40
+ orgUserId: string;
41
+ targetType: "course" | "activity" | "module";
42
+ targetId: string;
43
+ startedAt: unknown;
44
+ position: unknown;
45
+ completedAt: unknown;
46
+ updatedAt: unknown;
47
+ }>;
48
+ };
49
+ type ProgressStarted = EventOf<typeof progressEvents.progressStarted>;
50
+ type ProgressCompleted = EventOf<typeof progressEvents.progressCompleted>;
51
+ type ProgressEvent = EventOfValues<typeof progressEvents>;
52
+ type NewProgressEvent = NewDomainEvent<ProgressEvent>;
53
+ //#endregion
54
+ export { NewProgressEvent, ProgressCompleted, ProgressEvent, ProgressStarted, progressEvents };
55
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { progressRecordSchema } from "../types/schemas/progress.js";
2
+ import { defineEvent } from "../shared/events.js";
3
+ //#region src/progress/events.ts
4
+ const progressEvents = {
5
+ progressStarted: defineEvent({
6
+ type: "progress.record.started",
7
+ version: 1,
8
+ data: progressRecordSchema
9
+ }),
10
+ progressCompleted: defineEvent({
11
+ type: "progress.record.completed",
12
+ version: 1,
13
+ data: progressRecordSchema
14
+ })
15
+ };
16
+ //#endregion
17
+ export { progressEvents };
18
+
19
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../src/progress/events.ts"],"sourcesContent":["import { progressRecordSchema } from '../types/schemas/index.js';\nimport {\n defineEvent,\n type EventOf,\n type EventOfValues,\n type NewDomainEvent,\n} from '../shared/ports.js';\n\nexport const progressEvents = {\n progressStarted: defineEvent({\n type: 'progress.record.started',\n version: 1,\n data: progressRecordSchema,\n }),\n progressCompleted: defineEvent({\n type: 'progress.record.completed',\n version: 1,\n data: progressRecordSchema,\n }),\n};\n\nexport type ProgressStarted = EventOf<typeof progressEvents.progressStarted>;\nexport type ProgressCompleted = EventOf<typeof progressEvents.progressCompleted>;\nexport type ProgressEvent = EventOfValues<typeof progressEvents>;\nexport type NewProgressEvent = NewDomainEvent<ProgressEvent>;\n"],"mappings":";;;AAQA,MAAa,iBAAiB;CAC5B,iBAAiB,YAAY;EAC3B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,mBAAmB,YAAY;EAC7B,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;AACH"}
@@ -0,0 +1,8 @@
1
+ import { ProgressRecord, ProgressReportItem, ProgressTarget, ProgressTargetType, ReportProgressInput } from "../types/schemas/progress.js";
2
+ import { ProgressId } from "../types/progress.js";
3
+ import { ProgressCompleted, ProgressEvent, ProgressStarted, progressEvents } from "./events.js";
4
+ import "./model.js";
5
+ import "./types.js";
6
+ import { ProgressRepository, ProgressService, ProgressUnitOfWork } from "./ports.js";
7
+ import { ProgressServiceImpl } from "./service.js";
8
+ export { type ProgressCompleted, type ProgressEvent, type ProgressId, type ProgressRecord, type ProgressReportItem, type ProgressRepository, type ProgressService, ProgressServiceImpl, type ProgressStarted, type ProgressTarget, type ProgressTargetType, type ProgressUnitOfWork, type ReportProgressInput, progressEvents };
@@ -0,0 +1,3 @@
1
+ import { progressEvents } from "./events.js";
2
+ import { ProgressServiceImpl } from "./service.js";
3
+ export { ProgressServiceImpl, progressEvents };
@@ -0,0 +1,3 @@
1
+ import { ProgressRecord, ProgressTargetType } from "../types/schemas/progress.js";
2
+ import "../types/index.js";
3
+ export type { ProgressRecord, ProgressTargetType };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { ProgressRecord, ProgressTarget, ReportProgressInput } from "../types/schemas/progress.js";
2
+ import { OutboxAppender, UnitOfWork } from "../shared/ports.js";
3
+ import "./model.js";
4
+ import "./types.js";
5
+ //#region src/progress/ports.d.ts
6
+ interface ProgressService {
7
+ /** Process a usage report: ensure the record, apply position, evaluate
8
+ * completion (activity rule, then module/course against current structure),
9
+ * emit events. Returns the activity's record after the decision. */
10
+ report(orgId: string, input: ReportProgressInput): Promise<ProgressRecord>;
11
+ /** Fetch the record for a single (student, target), or null. */
12
+ get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null>;
13
+ /** Records for a set of target ids — the read the reporting layer composes. */
14
+ listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]>;
15
+ }
16
+ interface ProgressRepository {
17
+ /** Conflict-safe insert. null = lost a concurrent-insert race (unique key already taken). */
18
+ insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null>;
19
+ /** Scoped to the org — returns the record for the unique (student, target) key, or null. */
20
+ findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null>;
21
+ /** All of the student's records whose targetId is in the set. `forUpdate` takes tx-scoped
22
+ * row locks (ordered) to serialize concurrent reports for the same student+course. */
23
+ findByTargets(orgId: string, orgUserId: string, targetIds: string[], opts?: {
24
+ forUpdate?: boolean;
25
+ }): Promise<ProgressRecord[]>;
26
+ update(orgId: string, id: string, patch: Partial<Pick<ProgressRecord, 'position' | 'completedAt'>>): Promise<ProgressRecord | null>;
27
+ }
28
+ /** Writes that emit events run through this scope so row + outbox entry commit
29
+ * in one transaction. */
30
+ interface ProgressWriteScope {
31
+ progress: ProgressRepository;
32
+ outbox: OutboxAppender;
33
+ }
34
+ type ProgressUnitOfWork = UnitOfWork<ProgressWriteScope>;
35
+ //#endregion
36
+ export { ProgressRepository, ProgressService, ProgressUnitOfWork, ProgressWriteScope };
37
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { Logger } from "../types/ports.js";
2
+ import { ProgressRecord, ProgressTarget, ReportProgressInput } from "../types/schemas/progress.js";
3
+ import "../shared/ports.js";
4
+ import { ContentService } from "../content/ports.js";
5
+ import "../content/index.js";
6
+ import "./model.js";
7
+ import "./types.js";
8
+ import { ProgressRepository, ProgressService, ProgressUnitOfWork } from "./ports.js";
9
+ //#region src/progress/service.d.ts
10
+ type ProgressServiceParams = {
11
+ repo: ProgressRepository;
12
+ content: ContentService;
13
+ uow: ProgressUnitOfWork;
14
+ logger?: Logger;
15
+ };
16
+ declare class ProgressServiceImpl implements ProgressService {
17
+ private readonly repo;
18
+ private readonly content;
19
+ private readonly uow;
20
+ private readonly logger;
21
+ constructor(params: ProgressServiceParams);
22
+ report(orgId: string, input: ReportProgressInput): Promise<ProgressRecord>;
23
+ private ensureActivityRecord;
24
+ /** After an activity completes: newly-complete containers get their records
25
+ * (created complete) and a progress.completed event — same transaction. */
26
+ private completeContainers;
27
+ private ensureContainerComplete;
28
+ get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null>;
29
+ listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]>;
30
+ }
31
+ //#endregion
32
+ export { ProgressServiceImpl, ProgressServiceParams };
33
+ //# sourceMappingURL=service.d.ts.map