@headless-lms/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +66 -3
- package/dist/assets/events.js +19 -3
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/service.js +5 -3
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +11 -3
- package/dist/automations/actions.js +38 -5
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +4 -1
- package/dist/automations/catalog.js +289 -188
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/events.d.ts +46 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +5 -5
- package/dist/automations/index.js +2 -2
- package/dist/automations/model.d.ts +13 -3
- package/dist/automations/model.js +23 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +23 -8
- package/dist/automations/service.d.ts +21 -5
- package/dist/automations/service.js +267 -47
- package/dist/automations/service.js.map +1 -1
- package/dist/automations/types.d.ts +2 -2
- package/dist/automations/webhook-signing.d.ts +7 -0
- package/dist/automations/webhook-signing.js +22 -0
- package/dist/automations/webhook-signing.js.map +1 -0
- package/dist/content/events.d.ts +401 -25
- package/dist/content/events.js +125 -19
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +5 -5
- package/dist/content/index.js +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +42 -11
- package/dist/content/service.d.ts +15 -9
- package/dist/content/service.js +153 -45
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/events.d.ts +60 -4
- package/dist/discussion/events.js +30 -4
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/model.d.ts +1 -2
- package/dist/discussion/ports.d.ts +1 -1
- package/dist/discussion/service.d.ts +1 -1
- package/dist/discussion/service.js +11 -10
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +96 -12
- package/dist/entitlements/events.js +21 -4
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/service.js +1 -0
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/events.d.ts +17 -3
- package/dist/identity/events.js +12 -2
- package/dist/identity/events.js.map +1 -1
- package/dist/identity/ports.d.ts +3 -0
- package/dist/identity/service.d.ts +3 -0
- package/dist/identity/service.js +12 -1
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +27 -3
- package/dist/integrations/events.js +15 -3
- package/dist/integrations/events.js.map +1 -1
- package/dist/integrations/service.js +1 -1
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/events.d.ts +178 -11
- package/dist/organizations/events.js +52 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +3 -3
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +6 -10
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +9 -4
- package/dist/organizations/service.js +131 -72
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -2
- package/dist/progress/events.d.ts +34 -2
- package/dist/progress/events.js +15 -2
- package/dist/progress/events.js.map +1 -1
- package/dist/progress/service.d.ts +2 -2
- package/dist/progress/service.js +4 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/courses/index.d.ts +4 -0
- package/dist/reporting/courses/index.js +2 -0
- package/dist/reporting/courses/model.d.ts +24 -0
- package/dist/reporting/courses/model.js +1 -0
- package/dist/reporting/courses/ports.d.ts +13 -0
- package/dist/reporting/courses/ports.js +1 -0
- package/dist/reporting/courses/service.d.ts +19 -0
- package/dist/reporting/courses/service.js +20 -0
- package/dist/reporting/courses/service.js.map +1 -0
- package/dist/reporting/learn/model.d.ts +2 -2
- package/dist/reporting/learn/ports.d.ts +2 -1
- package/dist/reporting/learn/service.d.ts +2 -1
- package/dist/reporting/learn/service.js +5 -0
- package/dist/reporting/learn/service.js.map +1 -1
- package/dist/reporting/students/index.d.ts +2 -2
- package/dist/reporting/students/model.d.ts +21 -1
- package/dist/reporting/students/ports.d.ts +3 -1
- package/dist/reporting/students/service.d.ts +2 -1
- package/dist/reporting/students/service.js +3 -0
- package/dist/reporting/students/service.js.map +1 -1
- package/dist/shared/events.d.ts +5 -3
- package/dist/shared/events.js +1 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/id.d.ts +1 -0
- package/dist/shared/id.js +1 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +2 -0
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +13 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +3 -2
- package/dist/types/email-templates.d.ts +3 -1
- package/dist/types/events.d.ts +3 -3
- package/dist/types/index.d.ts +6 -6
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +1 -1
- package/dist/types/schemas/assets.d.ts +3 -0
- package/dist/types/schemas/assets.js +2 -1
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +71 -4
- package/dist/types/schemas/automations.js +35 -4
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +51 -18
- package/dist/types/schemas/content.js +40 -13
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/credentials.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +10 -2
- package/dist/types/schemas/discussion.js +12 -5
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +8 -2
- package/dist/types/schemas/entitlements.js +10 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +4 -4
- package/dist/types/schemas/identity.js +5 -5
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -6
- package/dist/types/schemas/index.js +6 -6
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +19 -5
- package/dist/types/schemas/organizations.js +12 -7
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/outbox.d.ts +1 -0
- package/dist/types/schemas/outbox.js +1 -0
- package/dist/types/schemas/outbox.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +1 -0
- package/dist/types/schemas/progress.js +1 -0
- package/dist/types/schemas/progress.js.map +1 -1
- package/dist/types/schemas/settings.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +2 -1
- package/dist/types/schemas/shared.js +3 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { genId } from "../shared/id.js";
|
|
2
1
|
import { noopLogger } from "../shared/logger.js";
|
|
3
|
-
import "../shared/
|
|
2
|
+
import { genId } from "../shared/id.js";
|
|
4
3
|
import { ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
4
|
+
import "../shared/settings.js";
|
|
5
5
|
import { discussionEvents } from "./events.js";
|
|
6
6
|
//#region src/discussion/service.ts
|
|
7
7
|
/** A course with no stored settings. Discussion is opt-in, so the common case
|
|
@@ -91,13 +91,12 @@ var DiscussionServiceImpl = class {
|
|
|
91
91
|
return this.toView(comment, authors, stored);
|
|
92
92
|
}
|
|
93
93
|
async hasAccess(orgId, activityId, actor) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})();
|
|
94
|
+
if (this.isStaff(actor)) return true;
|
|
95
|
+
const activity = await this.content.getActivity(orgId, activityId);
|
|
96
|
+
return activity ? this.access.hasCourseAccess(orgId, actor.id, activity.courseId) : false;
|
|
98
97
|
}
|
|
99
98
|
async postComment(orgId, { activityId, actor, parentId, body }) {
|
|
100
|
-
await this.hasAccess(orgId, activityId, actor);
|
|
99
|
+
if (!await this.hasAccess(orgId, activityId, actor)) throw new ForbiddenError("no access to this activity");
|
|
101
100
|
const config = await this.resolveConfig(orgId, activityId);
|
|
102
101
|
if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
|
|
103
102
|
if (parentId !== null) {
|
|
@@ -168,7 +167,7 @@ var DiscussionServiceImpl = class {
|
|
|
168
167
|
async remove(orgId, commentId, actor) {
|
|
169
168
|
const comment = await this.getComment(orgId, commentId);
|
|
170
169
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
171
|
-
await this.hasAccess(orgId, comment.activityId, actor);
|
|
170
|
+
if (!await this.hasAccess(orgId, comment.activityId, actor)) throw new ForbiddenError("no access to this activity");
|
|
172
171
|
if (comment.orgUserId !== actor.id && !this.isStaff(actor)) throw new ForbiddenError("only the author or a moderator may remove a comment");
|
|
173
172
|
if (comment.status === "removed") return comment;
|
|
174
173
|
return this.uow.run(async (scope) => {
|
|
@@ -207,7 +206,7 @@ var DiscussionServiceImpl = class {
|
|
|
207
206
|
async loadWithConfig(orgId, commentId, actor) {
|
|
208
207
|
const comment = await this.getComment(orgId, commentId);
|
|
209
208
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
210
|
-
await this.hasAccess(orgId, comment.activityId, actor);
|
|
209
|
+
if (!await this.hasAccess(orgId, comment.activityId, actor)) throw new ForbiddenError("no access to this activity");
|
|
211
210
|
return {
|
|
212
211
|
comment,
|
|
213
212
|
config: await this.resolveConfig(orgId, comment.activityId)
|
|
@@ -264,6 +263,7 @@ var DiscussionServiceImpl = class {
|
|
|
264
263
|
async reportComment(orgId, commentId, actor, reason) {
|
|
265
264
|
const { config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
266
265
|
if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
|
|
266
|
+
const now = /* @__PURE__ */ new Date();
|
|
267
267
|
const report = {
|
|
268
268
|
id: genId("commentReport"),
|
|
269
269
|
orgId,
|
|
@@ -271,7 +271,8 @@ var DiscussionServiceImpl = class {
|
|
|
271
271
|
orgUserId: actor.id,
|
|
272
272
|
reason,
|
|
273
273
|
resolvedAt: null,
|
|
274
|
-
createdAt:
|
|
274
|
+
createdAt: now,
|
|
275
|
+
updatedAt: now
|
|
275
276
|
};
|
|
276
277
|
return this.uow.run(async (scope) => {
|
|
277
278
|
const saved = await scope.discussion.insertReport(orgId, report);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type {\n ActivityCommentsRule,\n Comment,\n CommentAuthor,\n CommentReport,\n CommentSettings,\n} from './model.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorRecord,\n CommentReactions,\n CourseAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n PostCommentInput,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const;\n\n/** The slice of the course's stored settings this context reads. Absent for a\n * course that has never been configured, so the defaults apply. */\ninterface StoredCourseSettings {\n comments?: Partial<CommentSettings>;\n}\n\n/** The slice of an activity's opaque settings blob this context reads. Absent\n * or \"inherit\" = no override, so the course setting stands. */\ninterface StoredActivitySettings {\n comments?: ActivityCommentsRule;\n}\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: CourseAccessReader;\n content: ActivityGetter;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: CourseAccessReader;\n private readonly content: ActivityGetter;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.uow = params.uow;\n this.settings = params.settings;\n this.logger = params.logger ?? noopLogger;\n }\n\n async resolveConfig(orgId: string, activityId: string): Promise<CommentsConfig> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n\n // The `content` namespace row is the whole CourseSettings; comment settings\n // are one key inside it, written there by content.patchSettings.\n const stored = await this.settings.get<StoredCourseSettings>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity?.settings ?? {}) as StoredActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n /** Any org user that is not a learner moderates. The role arrives from\n * the edge already resolved; core never looks one up to authorise. */\n private isStaff(actor: Actor): boolean {\n return actor.role !== 'student';\n }\n\n private toAuthor(record: AuthorRecord): CommentAuthor {\n return {\n id: record.id,\n firstName: record.firstName,\n lastName: record.lastName,\n image: record.image,\n role: record.role,\n };\n }\n\n private toView(\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n reactions: CommentReactions,\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('OrgUser', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n parentId: comment.parentId,\n author: this.toAuthor(author),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? this.toAuthor(remover) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private async renderOne(orgId: string, comment: Comment, actor: Actor): Promise<CommentView> {\n const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(comment, authors, stored);\n }\n\n private async hasAccess(orgId: string, activityId: string, actor: Actor): Promise<boolean> {\n return (\n this.isStaff(actor) ||\n (await (async () => {\n const activity = await this.content.getActivity(orgId, activityId);\n return activity ? this.access.hasCourseAccess(orgId, actor.id, activity.courseId) : false;\n })())\n );\n }\n\n async postComment(\n orgId: string,\n { activityId, actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n await this.hasAccess(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('replies are disabled on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('replies nest one level');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('cannot reply to a comment that is not published');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !this.isStaff(actor) ? 'pending' : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: activityId,\n parentId: parentId,\n orgUserId: actor.id,\n body: body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({ orgId, data: row }),\n ]);\n this.logger.info('comment created', { orgId, commentId: row.id, status });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n const config = await this.resolveConfig(orgId, activityId);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(orgId, activityId);\n // A pending comment is visible to its own author and to staff, nobody else.\n const readable = rows.filter((c) => {\n if (c.status === 'published' || c.status === 'removed') {\n return true;\n }\n return this.isStaff(actor) || c.orgUserId === actor.id;\n });\n // Replies nest one level, so a reply has nothing hanging off it — a removed\n // reply is simply dropped rather than held open as a placeholder.\n const replies = readable.filter((c) => c.parentId !== null && c.status !== 'removed');\n const heldOpen = new Set(replies.map((r) => r.parentId));\n // A removed root survives only to hold replies THIS reader can see. Judging\n // it against every reply would show a marker with nothing beneath it.\n const roots = readable.filter(\n (c) => c.parentId === null && (c.status !== 'removed' || heldOpen.has(c.id)),\n );\n const rootIds = new Set(roots.map((r) => r.id));\n const served = [...roots, ...replies.filter((r) => rootIds.has(r.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n\n const [authors, reactions] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n config.reactions\n ? this.repo.reactionsOf(\n orgId,\n served.map((c) => c.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n ]);\n\n return {\n config,\n comments: served.map((c) => this.toView(c, authors, reactions[c.id] ?? EMPTY_REACTIONS)),\n };\n }\n\n async edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('only the author may edit a comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be edited');\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n body,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n\n await this.hasAccess(orgId, comment.activityId, actor);\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('only the author or a moderator may remove a comment');\n }\n\n if (comment.status === 'removed') {\n return comment;\n }\n\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({ orgId, data: updated }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, by: actor.id });\n return updated;\n });\n }\n\n async restore(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may restore a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', commentId);\n }\n return updated;\n }\n\n /** Load a comment together with its activity's resolved comments config,\n * having proved the actor may reach that activity at all. Every path that\n * acts on one comment needs the pair, and none should read the row twice. */\n private async loadWithConfig(\n orgId: string,\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n await this.hasAccess(orgId, comment.activityId, actor);\n const config = await this.resolveConfig(orgId, comment.activityId);\n return { comment, config };\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n // Writing to comments — locked and hidden both refuse.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError('reactions are disabled on this course');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be reacted to');\n }\n await this.repo.setReaction(orgId, commentId, actor.id, emoji);\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may approve a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('only a pending comment can be approved');\n }\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([\n discussionEvents.commentPublished.make({ orgId, data: updated }),\n ]);\n this.logger.info('comment published', { orgId, commentId });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. `approve` and `restore` still enforce\n * their own staff check; the check here covers the third path, where a\n * comment is already published and neither of them would run. */\n async publish(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may publish a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status === 'pending') {\n return this.approve(orgId, commentId, actor);\n }\n if (comment.status === 'removed') {\n return this.restore(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(orgId: string, commentId: string, actor: Actor): Promise<void> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may resolve a report');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n await this.repo.resolveReportsFor(orgId, commentId, new Date());\n this.logger.info('comment reports resolved', { orgId, commentId });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { config } = await this.loadWithConfig(orgId, commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n createdAt: new Date(),\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([\n discussionEvents.commentReported.make({ orgId, data: saved }),\n ]);\n this.logger.info('comment reported', { orgId, commentId });\n return saved;\n });\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>> {\n const page = await this.repo.listComments(orgId, query);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const authors = await this.repo.authorsOf(orgId, [\n ...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)]),\n ]);\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: comment.activityId,\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: this.toAuthor(author),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('OrgUser', r.orgUserId);\n }\n return {\n reporter: this.toAuthor(reporter),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Comment, CommentAuthor, CommentReport, CommentSettings } from './model.js';\nimport type { ActivitySettings, CourseSettings } from '../types/index.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorRecord,\n CommentReactions,\n CourseAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n PostCommentInput,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const satisfies CommentSettings;\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: CourseAccessReader;\n content: ActivityGetter;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: CourseAccessReader;\n private readonly content: ActivityGetter;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.uow = params.uow;\n this.settings = params.settings;\n this.logger = params.logger ?? noopLogger;\n }\n\n async resolveConfig(orgId: string, activityId: string): Promise<CommentsConfig> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n\n // The `content` namespace row is the whole CourseSettings; the `comments`\n // key inside it is owned by this context (commentSettingsSchema).\n const stored = await this.settings.get<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity?.settings ?? {}) as ActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n /** Any org user that is not a learner moderates. The role arrives from\n * the edge already resolved; core never looks one up to authorise. */\n private isStaff(actor: Actor): boolean {\n return actor.role !== 'student';\n }\n\n private toAuthor(record: AuthorRecord): CommentAuthor {\n return {\n id: record.id,\n firstName: record.firstName,\n lastName: record.lastName,\n image: record.image,\n role: record.role,\n };\n }\n\n private toView(\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n reactions: CommentReactions,\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('OrgUser', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n parentId: comment.parentId,\n author: this.toAuthor(author),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? this.toAuthor(remover) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private async renderOne(orgId: string, comment: Comment, actor: Actor): Promise<CommentView> {\n const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(comment, authors, stored);\n }\n\n private async hasAccess(orgId: string, activityId: string, actor: Actor): Promise<boolean> {\n if (this.isStaff(actor)) {\n return true;\n }\n const activity = await this.content.getActivity(orgId, activityId);\n return activity ? this.access.hasCourseAccess(orgId, actor.id, activity.courseId) : false;\n }\n\n async postComment(\n orgId: string,\n { activityId, actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n if (!(await this.hasAccess(orgId, activityId, actor))) {\n throw new ForbiddenError('no access to this activity');\n }\n const config = await this.resolveConfig(orgId, activityId);\n\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('replies are disabled on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('replies nest one level');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('cannot reply to a comment that is not published');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !this.isStaff(actor) ? 'pending' : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: activityId,\n parentId: parentId,\n orgUserId: actor.id,\n body: body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.outbox.append([discussionEvents.commentCreated.make({ orgId, data: row })]);\n this.logger.info('comment created', { orgId, commentId: row.id, status });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n const config = await this.resolveConfig(orgId, activityId);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(orgId, activityId);\n // A pending comment is visible to its own author and to staff, nobody else.\n const readable = rows.filter((c) => {\n if (c.status === 'published' || c.status === 'removed') {\n return true;\n }\n return this.isStaff(actor) || c.orgUserId === actor.id;\n });\n // Replies nest one level, so a reply has nothing hanging off it — a removed\n // reply is simply dropped rather than held open as a placeholder.\n const replies = readable.filter((c) => c.parentId !== null && c.status !== 'removed');\n const heldOpen = new Set(replies.map((r) => r.parentId));\n // A removed root survives only to hold replies THIS reader can see. Judging\n // it against every reply would show a marker with nothing beneath it.\n const roots = readable.filter(\n (c) => c.parentId === null && (c.status !== 'removed' || heldOpen.has(c.id)),\n );\n const rootIds = new Set(roots.map((r) => r.id));\n const served = [...roots, ...replies.filter((r) => rootIds.has(r.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n\n const [authors, reactions] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n config.reactions\n ? this.repo.reactionsOf(\n orgId,\n served.map((c) => c.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n ]);\n\n return {\n config,\n comments: served.map((c) => this.toView(c, authors, reactions[c.id] ?? EMPTY_REACTIONS)),\n };\n }\n\n async edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('only the author may edit a comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be edited');\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n body,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n\n if (!(await this.hasAccess(orgId, comment.activityId, actor))) {\n throw new ForbiddenError('no access to this activity');\n }\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('only the author or a moderator may remove a comment');\n }\n\n if (comment.status === 'removed') {\n return comment;\n }\n\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([discussionEvents.commentRemoved.make({ orgId, data: updated })]);\n this.logger.info('comment removed', { orgId, commentId, by: actor.id });\n return updated;\n });\n }\n\n async restore(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may restore a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', commentId);\n }\n return updated;\n }\n\n /** Load a comment together with its activity's resolved comments config,\n * having proved the actor may reach that activity at all. Every path that\n * acts on one comment needs the pair, and none should read the row twice. */\n private async loadWithConfig(\n orgId: string,\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (!(await this.hasAccess(orgId, comment.activityId, actor))) {\n throw new ForbiddenError('no access to this activity');\n }\n const config = await this.resolveConfig(orgId, comment.activityId);\n return { comment, config };\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n // Writing to comments — locked and hidden both refuse.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError('reactions are disabled on this course');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('a removed comment cannot be reacted to');\n }\n await this.repo.setReaction(orgId, commentId, actor.id, emoji);\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may approve a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('only a pending comment can be approved');\n }\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([discussionEvents.commentPublished.make({ orgId, data: updated })]);\n this.logger.info('comment published', { orgId, commentId });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. `approve` and `restore` still enforce\n * their own staff check; the check here covers the third path, where a\n * comment is already published and neither of them would run. */\n async publish(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may publish a comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status === 'pending') {\n return this.approve(orgId, commentId, actor);\n }\n if (comment.status === 'removed') {\n return this.restore(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(orgId: string, commentId: string, actor: Actor): Promise<void> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('only a moderator may resolve a report');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n await this.repo.resolveReportsFor(orgId, commentId, new Date());\n this.logger.info('comment reports resolved', { orgId, commentId });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { config } = await this.loadWithConfig(orgId, commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled) {\n throw new ForbiddenError('discussion is not open on this activity');\n }\n const now = new Date();\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n createdAt: now,\n updatedAt: now,\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([discussionEvents.commentReported.make({ orgId, data: saved })]);\n this.logger.info('comment reported', { orgId, commentId });\n return saved;\n });\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>> {\n const page = await this.repo.listComments(orgId, query);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const authors = await this.repo.authorsOf(orgId, [\n ...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)]),\n ]);\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('OrgUser', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: comment.activityId,\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: this.toAuthor(author),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('OrgUser', r.orgUserId);\n }\n return {\n reporter: this.toAuthor(reporter),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n}\n"],"mappings":"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"}
|
|
@@ -6,78 +6,162 @@ declare const entitlementEvents: {
|
|
|
6
6
|
orgId: string;
|
|
7
7
|
id: string;
|
|
8
8
|
orgUserId: string;
|
|
9
|
-
|
|
9
|
+
bundleId: string | null;
|
|
10
|
+
contentId: string | null;
|
|
10
11
|
status: "active" | "revoked";
|
|
11
12
|
source: string;
|
|
12
13
|
grantedAt: Date;
|
|
13
14
|
expiresAt: Date | null;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
updatedAt: Date;
|
|
14
17
|
}, "entitlement.created", 1>, {
|
|
15
18
|
orgId: string;
|
|
16
19
|
id: string;
|
|
17
20
|
orgUserId: string;
|
|
18
|
-
|
|
21
|
+
bundleId: string | null;
|
|
22
|
+
contentId: string | null;
|
|
19
23
|
status: "active" | "revoked";
|
|
20
24
|
source: string;
|
|
21
25
|
grantedAt: unknown;
|
|
22
26
|
expiresAt: unknown;
|
|
23
|
-
|
|
27
|
+
createdAt: unknown;
|
|
28
|
+
updatedAt: unknown;
|
|
29
|
+
}, import("zod").ZodObject<{
|
|
30
|
+
orgId: import("zod").ZodString;
|
|
31
|
+
id: import("zod").ZodString;
|
|
32
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
33
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
34
|
+
status: import("zod").ZodEnum<{
|
|
35
|
+
active: "active";
|
|
36
|
+
revoked: "revoked";
|
|
37
|
+
}>;
|
|
38
|
+
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
39
|
+
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
40
|
+
orgUserId: import("zod").ZodString;
|
|
41
|
+
source: import("zod").ZodString;
|
|
42
|
+
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
43
|
+
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
44
|
+
}, import("zod/v4/core").$strip>>;
|
|
24
45
|
entitlementUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
25
46
|
orgId: string;
|
|
26
47
|
id: string;
|
|
27
48
|
orgUserId: string;
|
|
28
|
-
|
|
49
|
+
bundleId: string | null;
|
|
50
|
+
contentId: string | null;
|
|
29
51
|
status: "active" | "revoked";
|
|
30
52
|
source: string;
|
|
31
53
|
grantedAt: Date;
|
|
32
54
|
expiresAt: Date | null;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
updatedAt: Date;
|
|
33
57
|
}, "entitlement.updated", 1>, {
|
|
34
58
|
orgId: string;
|
|
35
59
|
id: string;
|
|
36
60
|
orgUserId: string;
|
|
37
|
-
|
|
61
|
+
bundleId: string | null;
|
|
62
|
+
contentId: string | null;
|
|
38
63
|
status: "active" | "revoked";
|
|
39
64
|
source: string;
|
|
40
65
|
grantedAt: unknown;
|
|
41
66
|
expiresAt: unknown;
|
|
42
|
-
|
|
67
|
+
createdAt: unknown;
|
|
68
|
+
updatedAt: unknown;
|
|
69
|
+
}, import("zod").ZodObject<{
|
|
70
|
+
orgId: import("zod").ZodString;
|
|
71
|
+
id: import("zod").ZodString;
|
|
72
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
73
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
74
|
+
status: import("zod").ZodEnum<{
|
|
75
|
+
active: "active";
|
|
76
|
+
revoked: "revoked";
|
|
77
|
+
}>;
|
|
78
|
+
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
79
|
+
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
80
|
+
orgUserId: import("zod").ZodString;
|
|
81
|
+
source: import("zod").ZodString;
|
|
82
|
+
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
83
|
+
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
84
|
+
}, import("zod/v4/core").$strip>>;
|
|
43
85
|
entitlementDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
44
86
|
orgId: string;
|
|
45
87
|
id: string;
|
|
46
88
|
orgUserId: string;
|
|
47
|
-
|
|
89
|
+
bundleId: string | null;
|
|
90
|
+
contentId: string | null;
|
|
48
91
|
status: "active" | "revoked";
|
|
49
92
|
source: string;
|
|
50
93
|
grantedAt: Date;
|
|
51
94
|
expiresAt: Date | null;
|
|
95
|
+
createdAt: Date;
|
|
96
|
+
updatedAt: Date;
|
|
52
97
|
}, "entitlement.deleted", 1>, {
|
|
53
98
|
orgId: string;
|
|
54
99
|
id: string;
|
|
55
100
|
orgUserId: string;
|
|
56
|
-
|
|
101
|
+
bundleId: string | null;
|
|
102
|
+
contentId: string | null;
|
|
57
103
|
status: "active" | "revoked";
|
|
58
104
|
source: string;
|
|
59
105
|
grantedAt: unknown;
|
|
60
106
|
expiresAt: unknown;
|
|
61
|
-
|
|
107
|
+
createdAt: unknown;
|
|
108
|
+
updatedAt: unknown;
|
|
109
|
+
}, import("zod").ZodObject<{
|
|
110
|
+
orgId: import("zod").ZodString;
|
|
111
|
+
id: import("zod").ZodString;
|
|
112
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
113
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
114
|
+
status: import("zod").ZodEnum<{
|
|
115
|
+
active: "active";
|
|
116
|
+
revoked: "revoked";
|
|
117
|
+
}>;
|
|
118
|
+
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
119
|
+
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
120
|
+
orgUserId: import("zod").ZodString;
|
|
121
|
+
source: import("zod").ZodString;
|
|
122
|
+
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
123
|
+
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
124
|
+
}, import("zod/v4/core").$strip>>;
|
|
62
125
|
entitlementExpired: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
63
126
|
orgId: string;
|
|
64
127
|
id: string;
|
|
65
128
|
orgUserId: string;
|
|
66
|
-
|
|
129
|
+
bundleId: string | null;
|
|
130
|
+
contentId: string | null;
|
|
67
131
|
status: "active" | "revoked";
|
|
68
132
|
source: string;
|
|
69
133
|
grantedAt: Date;
|
|
70
134
|
expiresAt: Date | null;
|
|
135
|
+
createdAt: Date;
|
|
136
|
+
updatedAt: Date;
|
|
71
137
|
}, "entitlement.expired", 1>, {
|
|
72
138
|
orgId: string;
|
|
73
139
|
id: string;
|
|
74
140
|
orgUserId: string;
|
|
75
|
-
|
|
141
|
+
bundleId: string | null;
|
|
142
|
+
contentId: string | null;
|
|
76
143
|
status: "active" | "revoked";
|
|
77
144
|
source: string;
|
|
78
145
|
grantedAt: unknown;
|
|
79
146
|
expiresAt: unknown;
|
|
80
|
-
|
|
147
|
+
createdAt: unknown;
|
|
148
|
+
updatedAt: unknown;
|
|
149
|
+
}, import("zod").ZodObject<{
|
|
150
|
+
orgId: import("zod").ZodString;
|
|
151
|
+
id: import("zod").ZodString;
|
|
152
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
153
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
154
|
+
status: import("zod").ZodEnum<{
|
|
155
|
+
active: "active";
|
|
156
|
+
revoked: "revoked";
|
|
157
|
+
}>;
|
|
158
|
+
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
159
|
+
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
160
|
+
orgUserId: import("zod").ZodString;
|
|
161
|
+
source: import("zod").ZodString;
|
|
162
|
+
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
163
|
+
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
164
|
+
}, import("zod/v4/core").$strip>>;
|
|
81
165
|
};
|
|
82
166
|
type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;
|
|
83
167
|
type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;
|
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
import { entitlementSchema } from "../types/schemas/entitlements.js";
|
|
2
2
|
import { defineEvent } from "../shared/events.js";
|
|
3
3
|
//#region src/entitlements/events.ts
|
|
4
|
+
const publicEntitlement = entitlementSchema.pick({
|
|
5
|
+
orgId: true,
|
|
6
|
+
id: true,
|
|
7
|
+
orgUserId: true,
|
|
8
|
+
bundleId: true,
|
|
9
|
+
contentId: true,
|
|
10
|
+
status: true,
|
|
11
|
+
source: true,
|
|
12
|
+
grantedAt: true,
|
|
13
|
+
expiresAt: true,
|
|
14
|
+
createdAt: true,
|
|
15
|
+
updatedAt: true
|
|
16
|
+
}).strip();
|
|
4
17
|
const entitlementEvents = {
|
|
5
18
|
entitlementCreated: defineEvent({
|
|
6
19
|
type: "entitlement.created",
|
|
7
20
|
version: 1,
|
|
8
|
-
data: entitlementSchema
|
|
21
|
+
data: entitlementSchema,
|
|
22
|
+
publicData: publicEntitlement
|
|
9
23
|
}),
|
|
10
24
|
entitlementUpdated: defineEvent({
|
|
11
25
|
type: "entitlement.updated",
|
|
12
26
|
version: 1,
|
|
13
|
-
data: entitlementSchema
|
|
27
|
+
data: entitlementSchema,
|
|
28
|
+
publicData: publicEntitlement
|
|
14
29
|
}),
|
|
15
30
|
entitlementDeleted: defineEvent({
|
|
16
31
|
type: "entitlement.deleted",
|
|
17
32
|
version: 1,
|
|
18
|
-
data: entitlementSchema
|
|
33
|
+
data: entitlementSchema,
|
|
34
|
+
publicData: publicEntitlement
|
|
19
35
|
}),
|
|
20
36
|
entitlementExpired: defineEvent({
|
|
21
37
|
type: "entitlement.expired",
|
|
22
38
|
version: 1,
|
|
23
|
-
data: entitlementSchema
|
|
39
|
+
data: entitlementSchema,
|
|
40
|
+
publicData: publicEntitlement
|
|
24
41
|
})
|
|
25
42
|
};
|
|
26
43
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../src/entitlements/events.ts"],"sourcesContent":["import { entitlementSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nexport const entitlementEvents = {\n entitlementCreated: defineEvent({\n type: 'entitlement.created',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementUpdated: defineEvent({\n type: 'entitlement.updated',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementDeleted: defineEvent({\n type: 'entitlement.deleted',\n version: 1,\n data: entitlementSchema,\n }),\n entitlementExpired: defineEvent({\n type: 'entitlement.expired',\n version: 1,\n data: entitlementSchema,\n }),\n};\n\nexport type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;\nexport type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;\nexport type EntitlementDeleted = EventOf<typeof entitlementEvents.entitlementDeleted>;\nexport type EntitlementExpired = EventOf<typeof entitlementEvents.entitlementExpired>;\nexport type EntitlementEvent = EventOfValues<typeof entitlementEvents>;\n"],"mappings":";;;AAGA,MAAa,oBAAoB;CAC/B,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/entitlements/events.ts"],"sourcesContent":["import { entitlementSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nconst publicEntitlement = entitlementSchema\n .pick({\n orgId: true,\n id: true,\n orgUserId: true,\n bundleId: true,\n contentId: true,\n status: true,\n source: true,\n grantedAt: true,\n expiresAt: true,\n createdAt: true,\n updatedAt: true,\n })\n .strip();\n\nexport const entitlementEvents = {\n entitlementCreated: defineEvent({\n type: 'entitlement.created',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementUpdated: defineEvent({\n type: 'entitlement.updated',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementDeleted: defineEvent({\n type: 'entitlement.deleted',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n entitlementExpired: defineEvent({\n type: 'entitlement.expired',\n version: 1,\n data: entitlementSchema,\n publicData: publicEntitlement,\n }),\n};\n\nexport type EntitlementCreated = EventOf<typeof entitlementEvents.entitlementCreated>;\nexport type EntitlementUpdated = EventOf<typeof entitlementEvents.entitlementUpdated>;\nexport type EntitlementDeleted = EventOf<typeof entitlementEvents.entitlementDeleted>;\nexport type EntitlementExpired = EventOf<typeof entitlementEvents.entitlementExpired>;\nexport type EntitlementEvent = EventOfValues<typeof entitlementEvents>;\n"],"mappings":";;;AAGA,MAAM,oBAAoB,kBACvB,KAAK;CACJ,OAAO;CACP,IAAI;CACJ,WAAW;CACX,UAAU;CACV,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,WAAW;CACX,WAAW;CACX,WAAW;AACb,CAAC,CAAC,CACD,MAAM;AAET,MAAa,oBAAoB;CAC/B,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,oBAAoB,YAAY;EAC9B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/entitlements/service.ts"],"sourcesContent":["// entitlements context — service implementation (inbound port).\n//\n// Mutations run inside the context's UnitOfWork: the domain write and the\n// outbox append commit in ONE transaction (transactional outbox). This\n// service never publishes — the outbox relay republishes committed events to\n// the EventBus at-least-once.\nimport type { Entitlement, EntitlementsQuery, GrantEntitlementInput, Page } from './model.js';\nimport type {\n EntitlementsRepository,\n EntitlementsService,\n EntitlementsUnitOfWork,\n} from './ports.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { entitlementEvents } from './events.js';\n\nexport type EntitlementsServiceParams = {\n /** Read-only access (list) — runs outside any transaction. */\n repo: EntitlementsRepository;\n /** Atomic write scope: tx-bound repo + outbox appender. */\n uow: EntitlementsUnitOfWork;\n logger?: Logger;\n};\n\nexport class EntitlementsServiceImpl implements EntitlementsService {\n private readonly repo: EntitlementsRepository;\n private readonly uow: EntitlementsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: EntitlementsServiceParams) {\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n return this.repo.list(orgId, query);\n }\n\n async grant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insert(orgId, input);\n await outbox.append([\n entitlementEvents.entitlementCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('entitlement granted', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n contentId: entitlement.contentId,\n });\n return entitlement;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: 'active' | 'revoked',\n ): Promise<Entitlement | null> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const updated = await entitlements.setStatus(orgId, id, status);\n if (!updated) {\n return null;\n }\n await outbox.append([\n status === 'revoked'\n ? entitlementEvents.entitlementDeleted.make({\n orgId,\n data: updated,\n })\n : entitlementEvents.entitlementUpdated.make({\n orgId,\n data: updated,\n }),\n ]);\n return updated;\n });\n if (entitlement) {\n this.logger.info('entitlement status changed', { orgId, entitlementId: id, status });\n }\n return entitlement;\n }\n\n hasCourseAccess(orgId: string, orgUserId: string, courseId: string): Promise<boolean> {\n return this.repo.hasCourseAccess(orgId, orgUserId, courseId);\n }\n}\n"],"mappings":";;;AAwBA,IAAa,0BAAb,MAAoE;CAClE;CACA;CACA;CAEA,YAAY,QAAmC;EAC7C,KAAK,OAAO,OAAO;EACnB,KAAK,MAAM,OAAO;EAClB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,KAAK,OAAe,OAAsD;EACxE,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK;CACpC;CAEA,MAAM,MAAM,OAAe,OAAoD;EAC7E,MAAM,cAAc,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,cAAc,aAAa;GACzE,MAAM,UAAU,MAAM,aAAa,OAAO,OAAO,KAAK;GACtD,MAAM,OAAO,OAAO,CAClB,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,CACH,CAAC;GACD,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,uBAAuB;GACtC;GACA,eAAe,YAAY;GAC3B,WAAW,YAAY;GACvB,WAAW,YAAY;EACzB,CAAC;EACD,OAAO;CACT;CAEA,MAAM,UACJ,OACA,IACA,QAC6B;EAC7B,MAAM,cAAc,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,cAAc,aAAa;GACzE,MAAM,UAAU,MAAM,aAAa,UAAU,OAAO,IAAI,MAAM;GAC9D,IAAI,CAAC,SACH,OAAO;GAET,MAAM,OAAO,OAAO,CAClB,WAAW,YACP,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,IACD,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,CACP,CAAC;GACD,OAAO;EACT,CAAC;EACD,IAAI,aACF,KAAK,OAAO,KAAK,8BAA8B;GAAE;GAAO,eAAe;GAAI;EAAO,CAAC;EAErF,OAAO;CACT;CAEA,gBAAgB,OAAe,WAAmB,UAAoC;EACpF,OAAO,KAAK,KAAK,gBAAgB,OAAO,WAAW,QAAQ;CAC7D;AACF"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/entitlements/service.ts"],"sourcesContent":["// entitlements context — service implementation (inbound port).\n//\n// Mutations run inside the context's UnitOfWork: the domain write and the\n// outbox append commit in ONE transaction (transactional outbox). This\n// service never publishes — the outbox relay republishes committed events to\n// the EventBus at-least-once.\nimport type { Entitlement, EntitlementsQuery, GrantEntitlementInput, Page } from './model.js';\nimport type {\n EntitlementsRepository,\n EntitlementsService,\n EntitlementsUnitOfWork,\n} from './ports.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { entitlementEvents } from './events.js';\n\nexport type EntitlementsServiceParams = {\n /** Read-only access (list) — runs outside any transaction. */\n repo: EntitlementsRepository;\n /** Atomic write scope: tx-bound repo + outbox appender. */\n uow: EntitlementsUnitOfWork;\n logger?: Logger;\n};\n\nexport class EntitlementsServiceImpl implements EntitlementsService {\n private readonly repo: EntitlementsRepository;\n private readonly uow: EntitlementsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: EntitlementsServiceParams) {\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n return this.repo.list(orgId, query);\n }\n\n async grant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insert(orgId, input);\n await outbox.append([\n entitlementEvents.entitlementCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('entitlement granted', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n bundleId: entitlement.bundleId,\n contentId: entitlement.contentId,\n });\n return entitlement;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: 'active' | 'revoked',\n ): Promise<Entitlement | null> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const updated = await entitlements.setStatus(orgId, id, status);\n if (!updated) {\n return null;\n }\n await outbox.append([\n status === 'revoked'\n ? entitlementEvents.entitlementDeleted.make({\n orgId,\n data: updated,\n })\n : entitlementEvents.entitlementUpdated.make({\n orgId,\n data: updated,\n }),\n ]);\n return updated;\n });\n if (entitlement) {\n this.logger.info('entitlement status changed', { orgId, entitlementId: id, status });\n }\n return entitlement;\n }\n\n hasCourseAccess(orgId: string, orgUserId: string, courseId: string): Promise<boolean> {\n return this.repo.hasCourseAccess(orgId, orgUserId, courseId);\n }\n}\n"],"mappings":";;;AAwBA,IAAa,0BAAb,MAAoE;CAClE;CACA;CACA;CAEA,YAAY,QAAmC;EAC7C,KAAK,OAAO,OAAO;EACnB,KAAK,MAAM,OAAO;EAClB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,KAAK,OAAe,OAAsD;EACxE,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK;CACpC;CAEA,MAAM,MAAM,OAAe,OAAoD;EAC7E,MAAM,cAAc,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,cAAc,aAAa;GACzE,MAAM,UAAU,MAAM,aAAa,OAAO,OAAO,KAAK;GACtD,MAAM,OAAO,OAAO,CAClB,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,CACH,CAAC;GACD,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,uBAAuB;GACtC;GACA,eAAe,YAAY;GAC3B,WAAW,YAAY;GACvB,UAAU,YAAY;GACtB,WAAW,YAAY;EACzB,CAAC;EACD,OAAO;CACT;CAEA,MAAM,UACJ,OACA,IACA,QAC6B;EAC7B,MAAM,cAAc,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,cAAc,aAAa;GACzE,MAAM,UAAU,MAAM,aAAa,UAAU,OAAO,IAAI,MAAM;GAC9D,IAAI,CAAC,SACH,OAAO;GAET,MAAM,OAAO,OAAO,CAClB,WAAW,YACP,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,IACD,kBAAkB,mBAAmB,KAAK;IACxC;IACA,MAAM;GACR,CAAC,CACP,CAAC;GACD,OAAO;EACT,CAAC;EACD,IAAI,aACF,KAAK,OAAO,KAAK,8BAA8B;GAAE;GAAO,eAAe;GAAI;EAAO,CAAC;EAErF,OAAO;CACT;CAEA,gBAAgB,OAAe,WAAmB,UAAoC;EACpF,OAAO,KAAK,KAAK,gBAAgB,OAAO,WAAW,QAAQ;CAC7D;AACF"}
|
|
@@ -18,7 +18,14 @@ declare const identityEvents: {
|
|
|
18
18
|
lastName: string | null;
|
|
19
19
|
createdAt: unknown;
|
|
20
20
|
updatedAt: unknown;
|
|
21
|
-
}
|
|
21
|
+
}, import("zod").ZodObject<{
|
|
22
|
+
id: import("zod").ZodString;
|
|
23
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
24
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
25
|
+
email: import("zod").ZodEmail;
|
|
26
|
+
firstName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
27
|
+
lastName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
28
|
+
}, import("zod/v4/core").$strip>>;
|
|
22
29
|
userUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
23
30
|
id: string;
|
|
24
31
|
externalId: string | null;
|
|
@@ -35,7 +42,14 @@ declare const identityEvents: {
|
|
|
35
42
|
lastName: string | null;
|
|
36
43
|
createdAt: unknown;
|
|
37
44
|
updatedAt: unknown;
|
|
38
|
-
}
|
|
45
|
+
}, import("zod").ZodObject<{
|
|
46
|
+
id: import("zod").ZodString;
|
|
47
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
48
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
49
|
+
email: import("zod").ZodEmail;
|
|
50
|
+
firstName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
51
|
+
lastName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
52
|
+
}, import("zod/v4/core").$strip>>;
|
|
39
53
|
userDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
40
54
|
id: string;
|
|
41
55
|
externalId: string | null;
|
|
@@ -52,7 +66,7 @@ declare const identityEvents: {
|
|
|
52
66
|
lastName: string | null;
|
|
53
67
|
createdAt: unknown;
|
|
54
68
|
updatedAt: unknown;
|
|
55
|
-
}>;
|
|
69
|
+
}, undefined>;
|
|
56
70
|
};
|
|
57
71
|
type UserCreated = EventOf<typeof identityEvents.userCreated>;
|
|
58
72
|
type UserUpdated = EventOf<typeof identityEvents.userUpdated>;
|
package/dist/identity/events.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { userSchema } from "../types/schemas/identity.js";
|
|
2
2
|
import { defineEvent } from "../shared/events.js";
|
|
3
3
|
//#region src/identity/events.ts
|
|
4
|
+
const publicUser = userSchema.pick({
|
|
5
|
+
id: true,
|
|
6
|
+
email: true,
|
|
7
|
+
firstName: true,
|
|
8
|
+
lastName: true,
|
|
9
|
+
createdAt: true,
|
|
10
|
+
updatedAt: true
|
|
11
|
+
}).strip();
|
|
4
12
|
const identityEvents = {
|
|
5
13
|
userCreated: defineEvent({
|
|
6
14
|
type: "identity.user.created",
|
|
7
15
|
version: 1,
|
|
8
|
-
data: userSchema
|
|
16
|
+
data: userSchema,
|
|
17
|
+
publicData: publicUser
|
|
9
18
|
}),
|
|
10
19
|
userUpdated: defineEvent({
|
|
11
20
|
type: "identity.user.updated",
|
|
12
21
|
version: 1,
|
|
13
|
-
data: userSchema
|
|
22
|
+
data: userSchema,
|
|
23
|
+
publicData: publicUser
|
|
14
24
|
}),
|
|
15
25
|
userDeleted: defineEvent({
|
|
16
26
|
type: "identity.user.deleted",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../src/identity/events.ts"],"sourcesContent":["import { userSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nexport const identityEvents = {\n userCreated: defineEvent({\n type: 'identity.user.created',\n version: 1,\n data: userSchema,\n }),\n userUpdated: defineEvent({\n type: 'identity.user.updated',\n version: 1,\n data: userSchema,\n }),\n userDeleted: defineEvent({\n type: 'identity.user.deleted',\n version: 1,\n data: userSchema,\n }),\n};\n\nexport type UserCreated = EventOf<typeof identityEvents.userCreated>;\nexport type UserUpdated = EventOf<typeof identityEvents.userUpdated>;\nexport type UserDeleted = EventOf<typeof identityEvents.userDeleted>;\nexport type IdentityEvent = EventOfValues<typeof identityEvents>;\n"],"mappings":";;;AAGA,MAAa,iBAAiB;CAC5B,aAAa,YAAY;EACvB,MAAM;EACN,SAAS;EACT,MAAM;
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/identity/events.ts"],"sourcesContent":["import { userSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nconst publicUser = userSchema\n .pick({\n id: true,\n email: true,\n firstName: true,\n lastName: true,\n createdAt: true,\n updatedAt: true,\n })\n .strip();\n\nexport const identityEvents = {\n userCreated: defineEvent({\n type: 'identity.user.created',\n version: 1,\n data: userSchema,\n publicData: publicUser,\n }),\n userUpdated: defineEvent({\n type: 'identity.user.updated',\n version: 1,\n data: userSchema,\n publicData: publicUser,\n }),\n userDeleted: defineEvent({\n type: 'identity.user.deleted',\n version: 1,\n data: userSchema,\n }),\n};\n\nexport type UserCreated = EventOf<typeof identityEvents.userCreated>;\nexport type UserUpdated = EventOf<typeof identityEvents.userUpdated>;\nexport type UserDeleted = EventOf<typeof identityEvents.userDeleted>;\nexport type IdentityEvent = EventOfValues<typeof identityEvents>;\n"],"mappings":";;;AAGA,MAAM,aAAa,WAChB,KAAK;CACJ,IAAI;CACJ,OAAO;CACP,WAAW;CACX,UAAU;CACV,WAAW;CACX,WAAW;AACb,CAAC,CAAC,CACD,MAAM;AAET,MAAa,iBAAiB;CAC5B,aAAa,YAAY;EACvB,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,aAAa,YAAY;EACvB,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,aAAa,YAAY;EACvB,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;AACH"}
|
package/dist/identity/ports.d.ts
CHANGED
package/dist/identity/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { noopLogger } from "../shared/logger.js";
|
|
2
|
+
import { genId } from "../shared/id.js";
|
|
2
3
|
import { ConflictError, NotFoundError } from "../shared/errors.js";
|
|
3
4
|
import { identityEvents } from "./events.js";
|
|
4
5
|
//#region src/identity/service.ts
|
|
@@ -27,7 +28,14 @@ var IdentityServiceImpl = class {
|
|
|
27
28
|
}
|
|
28
29
|
async linkOrCreateUser(input) {
|
|
29
30
|
const existing = await this.repo.findUserByEmail(input.email);
|
|
30
|
-
if (!existing)
|
|
31
|
+
if (!existing) {
|
|
32
|
+
const id = input.id ?? genId("user");
|
|
33
|
+
return this.createUser({
|
|
34
|
+
...input,
|
|
35
|
+
id,
|
|
36
|
+
externalId: id
|
|
37
|
+
});
|
|
38
|
+
}
|
|
31
39
|
if (existing.externalId !== null) throw new ConflictError("That email already has an account");
|
|
32
40
|
const linked = await this.uow.run(async ({ identity, outbox }) => {
|
|
33
41
|
const user = await identity.updateUser(existing.id, {
|
|
@@ -69,6 +77,9 @@ var IdentityServiceImpl = class {
|
|
|
69
77
|
async sendPasswordReset(input) {
|
|
70
78
|
await this.mailer.send(input.email, "passwordReset", { resetUrl: input.url });
|
|
71
79
|
}
|
|
80
|
+
async sendPasswordChanged(input) {
|
|
81
|
+
await this.mailer.send(input.email, "passwordChanged", {});
|
|
82
|
+
}
|
|
72
83
|
async sendMagicLink(input) {
|
|
73
84
|
await this.mailer.send(input.email, "magicLink", { url: input.url });
|
|
74
85
|
}
|