@headless-lms/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +9 -9
- package/dist/assets/index.d.ts +3 -2
- package/dist/assets/index.js +2 -1
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +4 -3
- package/dist/assets/service.d.ts +0 -1
- package/dist/assets/service.js +0 -4
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +69 -62
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +4 -4
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +3 -17
- package/dist/automations/community-mail.js +1 -21
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +11 -4
- package/dist/automations/index.js +8 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +119 -5
- package/dist/automations/service.d.ts +10 -13
- package/dist/automations/service.js +21 -83
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +130 -80
- package/dist/content/events.js +11 -0
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +25 -6
- package/dist/content/service.d.ts +4 -4
- package/dist/content/service.js +24 -8
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/chats.d.ts +9 -4
- package/dist/discussion/chats.js +80 -47
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +194 -28
- package/dist/discussion/events.js +74 -17
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -2
- package/dist/discussion/index.js +6 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +107 -24
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +18 -8
- package/dist/discussion/posts.js +207 -155
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.d.ts +17 -8
- package/dist/discussion/service.js +105 -41
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +2 -1
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/ports.d.ts +7 -3
- package/dist/entitlements/service.d.ts +5 -2
- package/dist/entitlements/service.js +23 -3
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +5 -4
- package/dist/identity/index.js +3 -2
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +78 -54
- package/dist/integrations/events.d.ts +3 -3
- package/dist/organizations/events.d.ts +32 -32
- package/dist/organizations/index.d.ts +2 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +3 -0
- package/dist/organizations/service.d.ts +2 -0
- package/dist/organizations/service.js +7 -0
- package/dist/organizations/service.js.map +1 -1
- package/dist/progress/events.d.ts +14 -14
- package/dist/reporting/member/index.d.ts +2 -2
- package/dist/reporting/member/model.d.ts +56 -23
- package/dist/reporting/member/ports.d.ts +9 -6
- package/dist/reporting/member/service.d.ts +33 -14
- package/dist/reporting/member/service.js +138 -60
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/errors.d.ts +13 -1
- package/dist/shared/errors.js +18 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +3 -0
- package/dist/shared/id.js +4 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +2 -2
- package/dist/types/email-templates.d.ts +97 -3
- package/dist/types/identity.d.ts +2 -2
- package/dist/types/index.d.ts +7 -6
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- package/dist/types/schemas/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +62 -1
- package/dist/types/schemas/content.js +25 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +85 -12
- package/dist/types/schemas/discussion.js +47 -13
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -5
- package/dist/types/schemas/index.js +6 -5
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/progress.d.ts +3 -3
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Comment, CommentReport, CommentSettings } from './model.js';\nimport type { ActivitySettings, CourseSettings, Post, SpaceDetail } 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 ContentAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n PageQuery,\n PostCommentInput,\n PostService,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\nimport { authorView } from './authors.js';\nimport type { FileUrls } from '../assets/index.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: ContentAccessReader;\n content: ActivityGetter;\n posts: Pick<PostService, 'visible'>;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n communities: { listIds(orgId: string): Promise<string[]> };\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: ContentAccessReader;\n private readonly content: ActivityGetter;\n private readonly posts: Pick<PostService, 'visible'>;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly communities: { listIds(orgId: string): Promise<string[]> };\n private readonly fileUrls: FileUrls;\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.posts = params.posts;\n this.uow = params.uow;\n this.settings = params.settings;\n this.communities = params.communities;\n this.fileUrls = params.fileUrls;\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, owned and written\n // by content; this context only reads its `comments` key.\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 private isStaff(actor: Actor): boolean {\n return isStaffRole(actor.role);\n }\n\n private toView(\n orgId: string,\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n urls: FileUrls,\n reactions: CommentReactions,\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('Member', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n author: authorView(orgId, author, urls),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? authorView(orgId, remover, urls) : 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(orgId, comment, authors, this.fileUrls, stored);\n }\n\n private activityIdOf(comment: Comment): string {\n if (comment.activityId === null) {\n throw new NotFoundError('Activity', comment.id);\n }\n return comment.activityId;\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.hasAccess(orgId, actor.id, activity.courseId) : false;\n }\n\n private async requireActivityOpen(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<void> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n if (\n !this.isStaff(actor) &&\n !(await this.access.hasAccess(orgId, actor.id, activity.courseId))\n ) {\n throw new ForbiddenError('You cannot open this activity');\n }\n }\n\n async postComment(orgId: string, input: PostCommentInput): Promise<CommentView> {\n if (input.postId) {\n return this.postOnPost(orgId, input.postId, input);\n }\n const activityId = input.activityId;\n if (!activityId) {\n throw new NotFoundError('Activity', activityId ?? '');\n }\n return this.postOnActivity(orgId, activityId, input);\n }\n\n private async postOnActivity(\n orgId: string,\n activityId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n await this.requireActivityOpen(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 off 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('A reply cannot have replies of its own');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\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,\n postId: null,\n communityId: null,\n parentId,\n orgUserId: actor.id,\n 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, actorId: actor.id });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n private async postOnPost(\n orgId: string,\n postId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n if (post.status !== 'published') {\n throw new ConflictError('This post is not published');\n }\n if (!this.commentsOpen(post, space)) {\n throw new ForbiddenError('Comments are closed on this post');\n }\n if (parentId !== null) {\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.postId !== postId) {\n throw new NotFoundError('Comment', parentId);\n }\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n const now = new Date();\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: null,\n postId,\n communityId: post.communityId,\n parentId,\n orgUserId: actor.id,\n body,\n status: 'published',\n removedBy: null,\n createdAt: now,\n updatedAt: now,\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({\n orgId,\n data: row,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', {\n orgId,\n commentId: row.id,\n postId,\n status: 'published',\n actorId: actor.id,\n });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n const seesHeld = (person: Actor) => this.canModerate(person);\n const page = await this.repo.listThreadsByPost(orgId, postId, {\n ...query,\n viewerId: actor.id,\n seesHeld: seesHeld(actor),\n });\n const comments = await this.thread(\n orgId,\n page.rows,\n actor,\n post.reactionsEnabled ?? space.settings.reactions,\n seesHeld,\n );\n this.logger.debug('post comments read', { orgId, postId, count: comments.length });\n return { ...page, rows: comments };\n }\n\n private async thread(\n orgId: string,\n rows: Comment[],\n actor: Actor,\n reactionsOn: boolean,\n seesHeld: (actor: Actor) => boolean,\n ): Promise<CommentView[]> {\n const readable = rows.filter((comment) => {\n if (comment.status === 'published' || comment.status === 'removed') {\n return true;\n }\n return seesHeld(actor) || comment.orgUserId === actor.id;\n });\n const replies = readable.filter(\n (comment) => comment.parentId !== null && comment.status !== 'removed',\n );\n const heldOpen = new Set(replies.map((reply) => reply.parentId));\n const roots = readable.filter(\n (comment) =>\n comment.parentId === null && (comment.status !== 'removed' || heldOpen.has(comment.id)),\n );\n const rootIds = new Set(roots.map((root) => root.id));\n const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n const [authors, reactions] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n reactionsOn\n ? this.repo.reactionsOf(\n orgId,\n served.map((comment) => comment.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n ]);\n const urls = this.fileUrls;\n return served.map((comment) =>\n this.toView(orgId, comment, authors, urls, reactions[comment.id] ?? EMPTY_REACTIONS),\n );\n }\n\n private async visiblePost(\n orgId: string,\n actor: Actor,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n return this.posts.visible(orgId, actor, postId);\n }\n\n private async postOfComment(\n orgId: string,\n actor: Actor,\n commentId: string,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n try {\n return await this.visiblePost(orgId, actor, postId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new NotFoundError('Comment', commentId);\n }\n throw error;\n }\n }\n\n private canModerate(actor: Actor): boolean {\n return canModerate(actor.role);\n }\n\n private inCommunity(comment: Comment, communityId: string | null | undefined): boolean {\n return communityId === undefined || comment.communityId === communityId;\n }\n\n private commentsOpen(post: Post, space: SpaceDetail): boolean {\n return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n await this.requireActivityOpen(orgId, activityId, actor);\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 return {\n config,\n comments: await this.thread(orgId, rows, actor, config.reactions, (person) =>\n this.isStaff(person),\n ),\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(\n comment.postId\n ? 'Comments are closed on this post'\n : 'Discussion is not open on this activity',\n );\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('You can edit only your own 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(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.removeComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async removeComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment remove rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (comment.orgUserId !== actor.id && !this.canModerate(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n } else {\n if (!(await this.hasAccess(orgId, this.activityIdOf(comment), actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\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 if (comment.orgUserId !== actor.id) {\n await scope.discussion.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'removed',\n });\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({\n orgId,\n data: updated,\n metadata: updated.postId ? { actorOrgUserId: actor.id } : undefined,\n }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n async restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.restoreComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async restoreComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot restore this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment restore rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\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 this.logger.info('comment restored', { orgId, commentId, actorId: actor.id });\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 (comment.postId) {\n const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);\n const config: CommentsConfig = {\n enabled: this.commentsOpen(post, space),\n threaded: true,\n requireReview: false,\n reactions: post.reactionsEnabled ?? space.settings.reactions,\n };\n return { comment, config };\n }\n const activityId = this.activityIdOf(comment);\n if (!(await this.hasAccess(orgId, activityId, actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n const config = await this.resolveConfig(orgId, 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 if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError(\n comment.postId ? 'Reactions are off on this post' : 'Reactions are off on this course',\n );\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('You cannot approve this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n this.logger.warn('comment approve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('Only a comment awaiting review 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, actorId: actor.id });\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(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot publish this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment publish rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\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.restoreComment(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<void> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment reports resolve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (!this.canModerate(actor)) {\n throw new ForbiddenError('You cannot work this queue');\n }\n } else if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot resolve this report');\n }\n await this.repo.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'dismissed',\n });\n this.logger.info('comment reports resolved', { orgId, commentId, actorId: actor.id });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { comment, 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 && !comment.postId) {\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 resolution: null,\n resolvedBy: 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([\n discussionEvents.commentReported.make({\n orgId,\n data: { ...saved, communityId: comment.communityId },\n metadata: comment.postId ? { actorOrgUserId: actor.id } : undefined,\n }),\n ]);\n this.logger.info('comment reported', { orgId, commentId, actorId: actor.id });\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 const urls = this.fileUrls;\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('Member', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: this.activityIdOf(comment),\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: authorView(orgId, author, urls),\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('Member', r.orgUserId);\n }\n return {\n reporter: authorView(orgId, reporter, urls),\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 async sendDigest(orgId: string, now: Date): Promise<void> {\n const communityIds = await this.communities.listIds(orgId);\n if (communityIds.length === 0) {\n this.logger.debug('no communities for digest', { orgId });\n return;\n }\n const period = now.toISOString().slice(0, 10);\n await this.uow.run(async ({ outbox }) => {\n await outbox.append(\n communityIds.map((communityId) =>\n discussionEvents.digestDue.make({\n orgId,\n data: { communityId, period },\n }),\n ),\n );\n });\n this.logger.info('digest due', { orgId, communities: communityIds.length, period });\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 { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Comment, CommentReport, CommentSettings } from './model.js';\nimport type {\n Activity,\n ActivitySettings,\n CourseSettings,\n MentionView,\n Post,\n SpaceDetail,\n} 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 ContentAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n MentionsRepository,\n PageQuery,\n PostCommentInput,\n PostService,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\nimport { authorView } from './authors.js';\nimport { keepAllowed, mentionTokens, mentionViews } from './mentions.js';\nimport type { CommunityAccessReader } from '../entitlements/index.js';\nimport type { FileUrls } from '../assets/index.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: ContentAccessReader;\n content: ActivityGetter;\n posts: Pick<PostService, 'visible' | 'find'>;\n spaces: Pick<CommunityAccessReader, 'spaceOpenersAmong'>;\n mentions: MentionsRepository;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: ContentAccessReader;\n private readonly content: ActivityGetter;\n private readonly posts: Pick<PostService, 'visible' | 'find'>;\n private readonly spaces: Pick<CommunityAccessReader, 'spaceOpenersAmong'>;\n private readonly mentions: MentionsRepository;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly fileUrls: FileUrls;\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.posts = params.posts;\n this.spaces = params.spaces;\n this.mentions = params.mentions;\n this.uow = params.uow;\n this.settings = params.settings;\n this.fileUrls = params.fileUrls;\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, owned and written\n // by content; this context only reads its `comments` key.\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 private isStaff(actor: Actor): boolean {\n return isStaffRole(actor.role);\n }\n\n private toView(\n orgId: string,\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n urls: FileUrls,\n reactions: CommentReactions,\n mentions: MentionView[],\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('Member', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n author: authorView(orgId, author, urls),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? authorView(orgId, remover, urls) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n mentions: comment.status === 'removed' ? [] : mentions,\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, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn([comment])),\n mentionViews(this.mentions, this.repo, orgId, 'comment', [comment.id]),\n ]);\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(orgId, comment, authors, this.fileUrls, stored, mentioned[comment.id] ?? []);\n }\n\n private activityIdOf(comment: Comment): string {\n if (comment.activityId === null) {\n throw new NotFoundError('Activity', comment.id);\n }\n return comment.activityId;\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.hasAccess(orgId, actor.id, activity.courseId) : false;\n }\n\n private async requireActivityOpen(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<Activity> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n if (\n !this.isStaff(actor) &&\n !(await this.access.hasAccess(orgId, actor.id, activity.courseId))\n ) {\n throw new ForbiddenError('You cannot open this activity');\n }\n return activity;\n }\n\n private async courseOpeners(\n orgId: string,\n courseId: string,\n orgUserIds: string[],\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const people = await this.repo.authorsOf(orgId, orgUserIds);\n const opens = await Promise.all(\n orgUserIds.map(async (orgUserId) => {\n const person = people[orgUserId];\n if (!person) {\n return false;\n }\n return isStaffRole(person.role) || this.access.hasAccess(orgId, orgUserId, courseId);\n }),\n );\n return orgUserIds.filter((_, index) => opens[index]);\n }\n\n private async spaceOpeners(orgId: string, spaceId: string, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));\n }\n\n private async openersOf(orgId: string, comment: Comment, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n if (comment.postId) {\n const post = await this.posts.find(orgId, comment.postId);\n return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];\n }\n const activity = await this.content.getActivity(orgId, this.activityIdOf(comment));\n return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];\n }\n\n commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]> {\n return this.repo.commenterIds(orgId, target);\n }\n\n async postComment(orgId: string, input: PostCommentInput): Promise<CommentView> {\n if (input.postId) {\n return this.postOnPost(orgId, input.postId, input);\n }\n const activityId = input.activityId;\n if (!activityId) {\n throw new NotFoundError('Activity', activityId ?? '');\n }\n return this.postOnActivity(orgId, activityId, input);\n }\n\n private async postOnActivity(\n orgId: string,\n activityId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const activity = await this.requireActivityOpen(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 off 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('A reply cannot have replies of its own');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\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,\n postId: null,\n communityId: null,\n parentId,\n orgUserId: actor.id,\n body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, comment.createdAt);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: status === 'published' ? mentioned : [] },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', { orgId, commentId: row.id, status, actorId: actor.id });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n private async postOnPost(\n orgId: string,\n postId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n if (post.status !== 'published') {\n throw new ConflictError('This post is not published');\n }\n if (!this.commentsOpen(post, space)) {\n throw new ForbiddenError('Comments are closed on this post');\n }\n if (parentId !== null) {\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.postId !== postId) {\n throw new NotFoundError('Comment', parentId);\n }\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n const now = new Date();\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: null,\n postId,\n communityId: post.communityId,\n parentId,\n orgUserId: actor.id,\n body,\n status: 'published',\n removedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, now);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: mentioned },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', {\n orgId,\n commentId: row.id,\n postId,\n status: 'published',\n actorId: actor.id,\n });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n const seesHeld = (person: Actor) => this.canModerate(person);\n const page = await this.repo.listThreadsByPost(orgId, postId, {\n ...query,\n viewerId: actor.id,\n seesHeld: seesHeld(actor),\n });\n const comments = await this.thread(\n orgId,\n page.rows,\n actor,\n post.reactionsEnabled ?? space.settings.reactions,\n seesHeld,\n );\n this.logger.debug('post comments read', { orgId, postId, count: comments.length });\n return { ...page, rows: comments };\n }\n\n private async thread(\n orgId: string,\n rows: Comment[],\n actor: Actor,\n reactionsOn: boolean,\n seesHeld: (actor: Actor) => boolean,\n ): Promise<CommentView[]> {\n const readable = rows.filter((comment) => {\n if (comment.status === 'published' || comment.status === 'removed') {\n return true;\n }\n return seesHeld(actor) || comment.orgUserId === actor.id;\n });\n const replies = readable.filter(\n (comment) => comment.parentId !== null && comment.status !== 'removed',\n );\n const heldOpen = new Set(replies.map((reply) => reply.parentId));\n const roots = readable.filter(\n (comment) =>\n comment.parentId === null && (comment.status !== 'removed' || heldOpen.has(comment.id)),\n );\n const rootIds = new Set(roots.map((root) => root.id));\n const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n const [authors, reactions, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n reactionsOn\n ? this.repo.reactionsOf(\n orgId,\n served.map((comment) => comment.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n mentionViews(\n this.mentions,\n this.repo,\n orgId,\n 'comment',\n served.map((comment) => comment.id),\n ),\n ]);\n const urls = this.fileUrls;\n return served.map((comment) =>\n this.toView(\n orgId,\n comment,\n authors,\n urls,\n reactions[comment.id] ?? EMPTY_REACTIONS,\n mentioned[comment.id] ?? [],\n ),\n );\n }\n\n private async visiblePost(\n orgId: string,\n actor: Actor,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n return this.posts.visible(orgId, actor, postId);\n }\n\n private async postOfComment(\n orgId: string,\n actor: Actor,\n commentId: string,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n try {\n return await this.visiblePost(orgId, actor, postId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new NotFoundError('Comment', commentId);\n }\n throw error;\n }\n }\n\n private canModerate(actor: Actor): boolean {\n return canModerate(actor.role);\n }\n\n private inCommunity(comment: Comment, communityId: string | null | undefined): boolean {\n return communityId === undefined || comment.communityId === communityId;\n }\n\n private commentsOpen(post: Post, space: SpaceDetail): boolean {\n return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n await this.requireActivityOpen(orgId, activityId, actor);\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 return {\n config,\n comments: await this.thread(orgId, rows, actor, config.reactions, (person) =>\n this.isStaff(person),\n ),\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(\n comment.postId\n ? 'Comments are closed on this post'\n : 'Discussion is not open on this activity',\n );\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('You can edit only your own comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be edited');\n }\n const candidates = await this.openersOf(orgId, comment, mentionTokens(body));\n const updated = await this.uow.run(async (scope) => {\n const next = await scope.discussion.updateComment(orgId, commentId, {\n body,\n });\n if (!next) {\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 const added = await scope.mentions.sync(orgId, 'comment', commentId, candidates, new Date());\n await scope.outbox.append([\n discussionEvents.commentUpdated.make({\n orgId,\n data: {\n ...next,\n mentionedOrgUserIds: next.status === 'published' ? keepAllowed(candidates, added) : [],\n },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n return next;\n });\n this.logger.info('comment edited', { orgId, commentId, actorId: actor.id });\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.removeComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async removeComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment remove rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (comment.orgUserId !== actor.id && !this.canModerate(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n } else {\n if (!(await this.hasAccess(orgId, this.activityIdOf(comment), actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\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 if (comment.orgUserId !== actor.id) {\n await scope.discussion.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'removed',\n });\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({\n orgId,\n data: updated,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n async restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.restoreComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async restoreComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot restore this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment restore rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\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 this.logger.info('comment restored', { orgId, commentId, actorId: actor.id });\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; spaceId: string | null }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);\n const config: CommentsConfig = {\n enabled: this.commentsOpen(post, space),\n threaded: true,\n requireReview: false,\n reactions: post.reactionsEnabled ?? space.settings.reactions,\n };\n return { comment, config, spaceId: post.spaceId };\n }\n const activityId = this.activityIdOf(comment);\n if (!(await this.hasAccess(orgId, activityId, actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n const config = await this.resolveConfig(orgId, activityId);\n return { comment, config, spaceId: null };\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, spaceId } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError(\n comment.postId ? 'Reactions are off on this post' : 'Reactions are off on this course',\n );\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be reacted to');\n }\n await this.uow.run(async (scope) => {\n const first = await scope.discussion.setReaction(orgId, commentId, actor.id, emoji);\n if (!first || emoji === null) {\n return;\n }\n await scope.outbox.append([\n discussionEvents.commentReactionAdded.make({\n orgId,\n data: {\n commentId,\n postId: comment.postId,\n activityId: comment.activityId,\n spaceId,\n communityId: comment.communityId,\n orgUserId: actor.id,\n reaction: emoji,\n },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n });\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('You cannot approve this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n this.logger.warn('comment approve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('Only a comment awaiting review 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 const mentioned = await this.openersOf(\n orgId,\n updated,\n await scope.mentions.ofItem(orgId, 'comment', commentId),\n );\n await scope.outbox.append([\n discussionEvents.commentPublished.make({\n orgId,\n data: { ...updated, mentionedOrgUserIds: mentioned },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment published', { orgId, commentId, actorId: actor.id });\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(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot publish this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment publish rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\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.restoreComment(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<void> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment reports resolve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (!this.canModerate(actor)) {\n throw new ForbiddenError('You cannot work this queue');\n }\n } else if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot resolve this report');\n }\n await this.repo.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'dismissed',\n });\n this.logger.info('comment reports resolved', { orgId, commentId, actorId: actor.id });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { comment, 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 && !comment.postId) {\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 resolution: null,\n resolvedBy: 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([\n discussionEvents.commentReported.make({\n orgId,\n data: { ...saved, communityId: comment.communityId },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment reported', { orgId, commentId, actorId: actor.id });\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 const urls = this.fileUrls;\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('Member', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: this.activityIdOf(comment),\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: authorView(orgId, author, urls),\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('Member', r.orgUserId);\n }\n return {\n reporter: authorView(orgId, reporter, urls),\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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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Logger } from "../types/ports.js";
|
|
2
|
+
import { Subscription, SubscriptionItemType, SubscriptionView } from "../types/schemas/discussion.js";
|
|
3
|
+
import { SubjectRef } from "../types/notifications.js";
|
|
4
|
+
import "../types/index.js";
|
|
5
|
+
import "../shared/ports.js";
|
|
6
|
+
import { Actor } from "../shared/actor.js";
|
|
7
|
+
import { CommunityAccessReader } from "../entitlements/ports.js";
|
|
8
|
+
import "../entitlements/index.js";
|
|
9
|
+
import { ChatService, MuteInput, PostService, SubscriptionInput, SubscriptionService, SubscriptionsRepository } from "./ports.js";
|
|
10
|
+
//#region src/discussion/subscriptions.d.ts
|
|
11
|
+
declare function activeSubscription(subscription: Subscription | undefined, at: Date): SubscriptionView | null;
|
|
12
|
+
type SubscriptionServiceParams = {
|
|
13
|
+
repo: SubscriptionsRepository;
|
|
14
|
+
access: Pick<CommunityAccessReader, 'canOpenSpace' | 'isCommunityMember'>;
|
|
15
|
+
posts: Pick<PostService, 'visible'>;
|
|
16
|
+
chats: Pick<ChatService, 'chatSummary'>;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
};
|
|
19
|
+
declare class SubscriptionServiceImpl implements SubscriptionService {
|
|
20
|
+
private readonly repo;
|
|
21
|
+
private readonly access;
|
|
22
|
+
private readonly posts;
|
|
23
|
+
private readonly chats;
|
|
24
|
+
private readonly logger;
|
|
25
|
+
constructor(params: SubscriptionServiceParams);
|
|
26
|
+
setPost(orgId: string, actor: Actor, postId: string, input: SubscriptionInput): Promise<void>;
|
|
27
|
+
setSpace(orgId: string, actor: Actor, spaceId: string, input: MuteInput): Promise<void>;
|
|
28
|
+
setChat(orgId: string, actor: Actor, chatId: string, input: MuteInput): Promise<void>;
|
|
29
|
+
viewerSubscriptions(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[], at: Date): Promise<Record<string, SubscriptionView>>;
|
|
30
|
+
followerIds(orgId: string, postId: string): Promise<string[]>;
|
|
31
|
+
mutedAmong(orgId: string, item: SubjectRef, orgUserIds: string[], at: Date): Promise<string[]>;
|
|
32
|
+
itemSubscriptions(orgId: string, item: SubjectRef): Promise<Subscription[]>;
|
|
33
|
+
private write;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { SubscriptionServiceImpl, SubscriptionServiceParams, activeSubscription };
|
|
37
|
+
//# sourceMappingURL=subscriptions.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
2
|
+
import { noopLogger } from "../shared/logger.js";
|
|
3
|
+
//#region src/discussion/subscriptions.ts
|
|
4
|
+
const ITEM_TYPES = [
|
|
5
|
+
"post",
|
|
6
|
+
"space",
|
|
7
|
+
"chat"
|
|
8
|
+
];
|
|
9
|
+
function itemTypeOf(item) {
|
|
10
|
+
return ITEM_TYPES.includes(item.type) ? item.type : null;
|
|
11
|
+
}
|
|
12
|
+
function activeSubscription(subscription, at) {
|
|
13
|
+
if (!subscription) return null;
|
|
14
|
+
if (subscription.state === "muted" && subscription.mutedUntil !== null && subscription.mutedUntil <= at) return null;
|
|
15
|
+
return {
|
|
16
|
+
state: subscription.state,
|
|
17
|
+
mutedUntil: subscription.mutedUntil
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
var SubscriptionServiceImpl = class {
|
|
21
|
+
repo;
|
|
22
|
+
access;
|
|
23
|
+
posts;
|
|
24
|
+
chats;
|
|
25
|
+
logger;
|
|
26
|
+
constructor(params) {
|
|
27
|
+
this.repo = params.repo;
|
|
28
|
+
this.access = params.access;
|
|
29
|
+
this.posts = params.posts;
|
|
30
|
+
this.chats = params.chats;
|
|
31
|
+
this.logger = params.logger ?? noopLogger;
|
|
32
|
+
}
|
|
33
|
+
async setPost(orgId, actor, postId, input) {
|
|
34
|
+
await this.posts.visible(orgId, actor, postId);
|
|
35
|
+
await this.write(orgId, actor, "post", postId, input);
|
|
36
|
+
}
|
|
37
|
+
async setSpace(orgId, actor, spaceId, input) {
|
|
38
|
+
if (!await this.access.canOpenSpace(orgId, actor, spaceId)) throw new NotFoundError("Space", spaceId);
|
|
39
|
+
await this.write(orgId, actor, "space", spaceId, input);
|
|
40
|
+
}
|
|
41
|
+
async setChat(orgId, actor, chatId, input) {
|
|
42
|
+
const summary = await this.chats.chatSummary(orgId, chatId);
|
|
43
|
+
if (!summary) throw new NotFoundError("Chat", chatId);
|
|
44
|
+
if (!await this.access.isCommunityMember(orgId, actor, summary.chat.communityId)) throw new ForbiddenError("You are not a member of this community");
|
|
45
|
+
if (!summary.participants.some((user) => user.orgUserId === actor.id && user.leftAt === null)) throw new ForbiddenError("You are not in this chat");
|
|
46
|
+
await this.write(orgId, actor, "chat", chatId, input);
|
|
47
|
+
}
|
|
48
|
+
async viewerSubscriptions(orgId, orgUserId, itemType, itemIds, at) {
|
|
49
|
+
if (itemIds.length === 0) return {};
|
|
50
|
+
const rows = await this.repo.forViewer(orgId, orgUserId, itemType, [...new Set(itemIds)]);
|
|
51
|
+
return Object.fromEntries(rows.flatMap((row) => {
|
|
52
|
+
const view = activeSubscription(row, at);
|
|
53
|
+
return view ? [[row.itemId, view]] : [];
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
followerIds(orgId, postId) {
|
|
57
|
+
return this.repo.followerIds(orgId, postId);
|
|
58
|
+
}
|
|
59
|
+
async mutedAmong(orgId, item, orgUserIds, at) {
|
|
60
|
+
const itemType = itemTypeOf(item);
|
|
61
|
+
if (itemType === null || orgUserIds.length === 0) return [];
|
|
62
|
+
return this.repo.mutedAmong(orgId, itemType, item.id, [...new Set(orgUserIds)], at);
|
|
63
|
+
}
|
|
64
|
+
async itemSubscriptions(orgId, item) {
|
|
65
|
+
const itemType = itemTypeOf(item);
|
|
66
|
+
return itemType === null ? [] : this.repo.ofItem(orgId, itemType, item.id);
|
|
67
|
+
}
|
|
68
|
+
async write(orgId, actor, itemType, itemId, input) {
|
|
69
|
+
if (input.state === "none") {
|
|
70
|
+
await this.repo.remove(orgId, actor.id, itemType, itemId);
|
|
71
|
+
this.logger.debug("subscription removed", {
|
|
72
|
+
orgId,
|
|
73
|
+
itemType,
|
|
74
|
+
itemId
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (input.state === "following" && itemType !== "post") throw new InvalidInputError("Only a post can be followed");
|
|
79
|
+
await this.repo.upsert(orgId, {
|
|
80
|
+
orgId,
|
|
81
|
+
orgUserId: actor.id,
|
|
82
|
+
itemType,
|
|
83
|
+
itemId,
|
|
84
|
+
state: input.state,
|
|
85
|
+
mutedUntil: input.state === "muted" ? input.mutedUntil ?? null : null,
|
|
86
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
87
|
+
});
|
|
88
|
+
this.logger.debug("subscription set", {
|
|
89
|
+
orgId,
|
|
90
|
+
itemType,
|
|
91
|
+
itemId,
|
|
92
|
+
state: input.state
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
export { SubscriptionServiceImpl, activeSubscription };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=subscriptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.js","names":[],"sources":["../../src/discussion/subscriptions.ts"],"sourcesContent":["import { ForbiddenError, InvalidInputError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { CommunityAccessReader } from '../entitlements/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type {\n SubjectRef,\n Subscription,\n SubscriptionItemType,\n SubscriptionView,\n} from '../types/index.js';\nimport type {\n ChatService,\n MuteInput,\n PostService,\n SubscriptionInput,\n SubscriptionService,\n SubscriptionsRepository,\n} from './ports.js';\n\nconst ITEM_TYPES: readonly SubscriptionItemType[] = ['post', 'space', 'chat'];\n\nfunction itemTypeOf(item: SubjectRef): SubscriptionItemType | null {\n return (ITEM_TYPES as readonly string[]).includes(item.type)\n ? (item.type as SubscriptionItemType)\n : null;\n}\n\nexport function activeSubscription(\n subscription: Subscription | undefined,\n at: Date,\n): SubscriptionView | null {\n if (!subscription) {\n return null;\n }\n if (\n subscription.state === 'muted' &&\n subscription.mutedUntil !== null &&\n subscription.mutedUntil <= at\n ) {\n return null;\n }\n return { state: subscription.state, mutedUntil: subscription.mutedUntil };\n}\n\nexport type SubscriptionServiceParams = {\n repo: SubscriptionsRepository;\n access: Pick<CommunityAccessReader, 'canOpenSpace' | 'isCommunityMember'>;\n posts: Pick<PostService, 'visible'>;\n chats: Pick<ChatService, 'chatSummary'>;\n logger?: Logger;\n};\n\nexport class SubscriptionServiceImpl implements SubscriptionService {\n private readonly repo: SubscriptionsRepository;\n private readonly access: Pick<CommunityAccessReader, 'canOpenSpace' | 'isCommunityMember'>;\n private readonly posts: Pick<PostService, 'visible'>;\n private readonly chats: Pick<ChatService, 'chatSummary'>;\n private readonly logger: Logger;\n\n constructor(params: SubscriptionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.posts = params.posts;\n this.chats = params.chats;\n this.logger = params.logger ?? noopLogger;\n }\n\n async setPost(\n orgId: string,\n actor: Actor,\n postId: string,\n input: SubscriptionInput,\n ): Promise<void> {\n await this.posts.visible(orgId, actor, postId);\n await this.write(orgId, actor, 'post', postId, input);\n }\n\n async setSpace(orgId: string, actor: Actor, spaceId: string, input: MuteInput): Promise<void> {\n if (!(await this.access.canOpenSpace(orgId, actor, spaceId))) {\n throw new NotFoundError('Space', spaceId);\n }\n await this.write(orgId, actor, 'space', spaceId, input);\n }\n\n async setChat(orgId: string, actor: Actor, chatId: string, input: MuteInput): Promise<void> {\n const summary = await this.chats.chatSummary(orgId, chatId);\n if (!summary) {\n throw new NotFoundError('Chat', chatId);\n }\n if (!(await this.access.isCommunityMember(orgId, actor, summary.chat.communityId))) {\n throw new ForbiddenError('You are not a member of this community');\n }\n if (!summary.participants.some((user) => user.orgUserId === actor.id && user.leftAt === null)) {\n throw new ForbiddenError('You are not in this chat');\n }\n await this.write(orgId, actor, 'chat', chatId, input);\n }\n\n async viewerSubscriptions(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemIds: string[],\n at: Date,\n ): Promise<Record<string, SubscriptionView>> {\n if (itemIds.length === 0) {\n return {};\n }\n const rows = await this.repo.forViewer(orgId, orgUserId, itemType, [...new Set(itemIds)]);\n return Object.fromEntries(\n rows.flatMap((row) => {\n const view = activeSubscription(row, at);\n return view ? [[row.itemId, view]] : [];\n }),\n );\n }\n\n followerIds(orgId: string, postId: string): Promise<string[]> {\n return this.repo.followerIds(orgId, postId);\n }\n\n async mutedAmong(\n orgId: string,\n item: SubjectRef,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]> {\n const itemType = itemTypeOf(item);\n if (itemType === null || orgUserIds.length === 0) {\n return [];\n }\n return this.repo.mutedAmong(orgId, itemType, item.id, [...new Set(orgUserIds)], at);\n }\n\n async itemSubscriptions(orgId: string, item: SubjectRef): Promise<Subscription[]> {\n const itemType = itemTypeOf(item);\n return itemType === null ? [] : this.repo.ofItem(orgId, itemType, item.id);\n }\n\n private async write(\n orgId: string,\n actor: Actor,\n itemType: SubscriptionItemType,\n itemId: string,\n input: SubscriptionInput,\n ): Promise<void> {\n if (input.state === 'none') {\n await this.repo.remove(orgId, actor.id, itemType, itemId);\n this.logger.debug('subscription removed', { orgId, itemType, itemId });\n return;\n }\n if (input.state === 'following' && itemType !== 'post') {\n throw new InvalidInputError('Only a post can be followed');\n }\n await this.repo.upsert(orgId, {\n orgId,\n orgUserId: actor.id,\n itemType,\n itemId,\n state: input.state,\n mutedUntil: input.state === 'muted' ? (input.mutedUntil ?? null) : null,\n createdAt: new Date(),\n });\n this.logger.debug('subscription set', { orgId, itemType, itemId, state: input.state });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SubjectRef } from "../types/notifications.js";
|
|
2
|
+
import "../types/index.js";
|
|
3
|
+
import { Actor } from "../shared/actor.js";
|
|
4
|
+
import { ActivityGetter, AuthorProfileReader, ChatRepository, ChatService, ContentAccessReader, DiscussionRepository, ItemTitle, ItemTitles, PostService } from "./ports.js";
|
|
5
|
+
//#region src/discussion/titles.d.ts
|
|
6
|
+
type DiscussionItemTitlesParams = {
|
|
7
|
+
posts: Pick<PostService, 'visibleTitles'>;
|
|
8
|
+
chats: Pick<ChatService, 'titlesFor'>;
|
|
9
|
+
comments: Pick<DiscussionRepository, 'findComment'>;
|
|
10
|
+
messages: Pick<ChatRepository, 'findMessage'>;
|
|
11
|
+
profiles: AuthorProfileReader;
|
|
12
|
+
activities: ActivityGetter;
|
|
13
|
+
access: ContentAccessReader;
|
|
14
|
+
};
|
|
15
|
+
declare class DiscussionItemTitles implements ItemTitles {
|
|
16
|
+
private readonly deps;
|
|
17
|
+
constructor(deps: DiscussionItemTitlesParams);
|
|
18
|
+
titlesFor(orgId: string, actor: Actor, refs: SubjectRef[]): Promise<Record<string, ItemTitle>>;
|
|
19
|
+
private found;
|
|
20
|
+
private seesComment;
|
|
21
|
+
private openActivities;
|
|
22
|
+
private names;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { DiscussionItemTitles, DiscussionItemTitlesParams };
|
|
26
|
+
//# sourceMappingURL=titles.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
2
|
+
import { activityTitle } from "../content/titles.js";
|
|
3
|
+
import { authorView } from "./authors.js";
|
|
4
|
+
import { mentionTokens } from "./mentions.js";
|
|
5
|
+
//#region src/discussion/titles.ts
|
|
6
|
+
const COMMENT_EXCERPT = 280;
|
|
7
|
+
const MESSAGE_EXCERPT = 140;
|
|
8
|
+
const TOKEN = /<@([A-Za-z0-9_-]+)>/g;
|
|
9
|
+
function idsOf(refs, type) {
|
|
10
|
+
return [...new Set(refs.filter((ref) => ref.type === type).map((ref) => ref.id))];
|
|
11
|
+
}
|
|
12
|
+
function excerpt(body, names, max) {
|
|
13
|
+
const flat = body.replace(TOKEN, (_, id) => `@${names[id] ?? "A member"}`).replace(/\s+/g, " ").trim();
|
|
14
|
+
return flat.length <= max ? flat : flat.slice(0, max);
|
|
15
|
+
}
|
|
16
|
+
var DiscussionItemTitles = class {
|
|
17
|
+
deps;
|
|
18
|
+
constructor(deps) {
|
|
19
|
+
this.deps = deps;
|
|
20
|
+
}
|
|
21
|
+
async titlesFor(orgId, actor, refs) {
|
|
22
|
+
const [comments, messages] = await Promise.all([this.found(idsOf(refs, "comment"), (id) => this.deps.comments.findComment(orgId, id)), this.found(idsOf(refs, "message"), (id) => this.deps.messages.findMessage(orgId, id))]);
|
|
23
|
+
const shownComments = comments.filter((comment) => this.seesComment(actor, comment));
|
|
24
|
+
const shownMessages = messages.filter((message) => message.deletedAt === null);
|
|
25
|
+
const postIds = [...idsOf(refs, "post"), ...shownComments.flatMap((comment) => comment.postId ?? [])];
|
|
26
|
+
const chatIds = [...idsOf(refs, "chat"), ...shownMessages.map((message) => message.chatId)];
|
|
27
|
+
const [posts, chats, activities, names] = await Promise.all([
|
|
28
|
+
postIds.length > 0 ? this.deps.posts.visibleTitles(orgId, actor, postIds) : {},
|
|
29
|
+
chatIds.length > 0 ? this.deps.chats.titlesFor(orgId, actor, chatIds) : {},
|
|
30
|
+
this.openActivities(orgId, actor, shownComments),
|
|
31
|
+
this.names(orgId, [...shownComments, ...shownMessages].map((item) => item.body))
|
|
32
|
+
]);
|
|
33
|
+
const titles = {};
|
|
34
|
+
for (const id of idsOf(refs, "post")) {
|
|
35
|
+
const title = posts[id];
|
|
36
|
+
if (title !== void 0) titles[`post:${id}`] = { title };
|
|
37
|
+
}
|
|
38
|
+
for (const id of idsOf(refs, "chat")) {
|
|
39
|
+
const title = chats[id];
|
|
40
|
+
if (title !== void 0) titles[`chat:${id}`] = { title };
|
|
41
|
+
}
|
|
42
|
+
for (const comment of shownComments) {
|
|
43
|
+
const title = comment.postId ? posts[comment.postId] : comment.activityId ? activities[comment.activityId] : void 0;
|
|
44
|
+
if (title !== void 0) titles[`comment:${comment.id}`] = {
|
|
45
|
+
title,
|
|
46
|
+
excerpt: excerpt(comment.body, names, COMMENT_EXCERPT)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
for (const message of shownMessages) {
|
|
50
|
+
const title = chats[message.chatId];
|
|
51
|
+
if (title !== void 0) titles[`message:${message.id}`] = {
|
|
52
|
+
title,
|
|
53
|
+
excerpt: excerpt(message.body, names, MESSAGE_EXCERPT)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return titles;
|
|
57
|
+
}
|
|
58
|
+
async found(ids, find) {
|
|
59
|
+
return (await Promise.all(ids.map(find))).filter((row) => row !== null);
|
|
60
|
+
}
|
|
61
|
+
seesComment(actor, comment) {
|
|
62
|
+
if (comment.status === "published" || canModerate(actor.role)) return true;
|
|
63
|
+
return comment.orgUserId === actor.id && comment.status === "pending";
|
|
64
|
+
}
|
|
65
|
+
async openActivities(orgId, actor, comments) {
|
|
66
|
+
const ids = [...new Set(comments.flatMap((comment) => comment.postId ? [] : comment.activityId ?? []))];
|
|
67
|
+
const open = await Promise.all(ids.map(async (id) => {
|
|
68
|
+
const activity = await this.deps.activities.getActivity(orgId, id);
|
|
69
|
+
if (!activity) return null;
|
|
70
|
+
return isStaffRole(actor.role) || await this.deps.access.hasAccess(orgId, actor.id, activity.courseId) ? [id, activityTitle(activity)] : null;
|
|
71
|
+
}));
|
|
72
|
+
return Object.fromEntries(open.filter((entry) => entry !== null));
|
|
73
|
+
}
|
|
74
|
+
async names(orgId, bodies) {
|
|
75
|
+
const ids = mentionTokens(...bodies);
|
|
76
|
+
if (ids.length === 0) return {};
|
|
77
|
+
const records = await this.deps.profiles.authorsOf(orgId, ids);
|
|
78
|
+
return Object.fromEntries(Object.entries(records).map(([id, record]) => [id, authorView(orgId, record, null).displayName || "A member"]));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { DiscussionItemTitles };
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=titles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"titles.js","names":[],"sources":["../../src/discussion/titles.ts"],"sourcesContent":["import { activityTitle } from '../content/index.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { SubjectRef } from '../types/index.js';\nimport { authorView } from './authors.js';\nimport { mentionTokens } from './mentions.js';\nimport type {\n ActivityGetter,\n AuthorProfileReader,\n ChatRepository,\n ChatService,\n ContentAccessReader,\n DiscussionRepository,\n ItemTitle,\n ItemTitles,\n PostService,\n} from './ports.js';\nimport type { Comment } from '../types/index.js';\n\nconst COMMENT_EXCERPT = 280;\nconst MESSAGE_EXCERPT = 140;\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\n\nexport type DiscussionItemTitlesParams = {\n posts: Pick<PostService, 'visibleTitles'>;\n chats: Pick<ChatService, 'titlesFor'>;\n comments: Pick<DiscussionRepository, 'findComment'>;\n messages: Pick<ChatRepository, 'findMessage'>;\n profiles: AuthorProfileReader;\n activities: ActivityGetter;\n access: ContentAccessReader;\n};\n\nfunction idsOf(refs: SubjectRef[], type: SubjectRef['type']): string[] {\n return [...new Set(refs.filter((ref) => ref.type === type).map((ref) => ref.id))];\n}\n\nfunction excerpt(body: string, names: Record<string, string>, max: number): string {\n const named = body.replace(TOKEN, (_, id: string) => `@${names[id] ?? 'A member'}`);\n const flat = named.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max);\n}\n\nexport class DiscussionItemTitles implements ItemTitles {\n constructor(private readonly deps: DiscussionItemTitlesParams) {}\n\n async titlesFor(\n orgId: string,\n actor: Actor,\n refs: SubjectRef[],\n ): Promise<Record<string, ItemTitle>> {\n const [comments, messages] = await Promise.all([\n this.found(idsOf(refs, 'comment'), (id) => this.deps.comments.findComment(orgId, id)),\n this.found(idsOf(refs, 'message'), (id) => this.deps.messages.findMessage(orgId, id)),\n ]);\n const shownComments = comments.filter((comment) => this.seesComment(actor, comment));\n const shownMessages = messages.filter((message) => message.deletedAt === null);\n const postIds = [\n ...idsOf(refs, 'post'),\n ...shownComments.flatMap((comment) => comment.postId ?? []),\n ];\n const chatIds = [...idsOf(refs, 'chat'), ...shownMessages.map((message) => message.chatId)];\n const [posts, chats, activities, names] = await Promise.all([\n postIds.length > 0\n ? this.deps.posts.visibleTitles(orgId, actor, postIds)\n : ({} as Record<string, string>),\n chatIds.length > 0\n ? this.deps.chats.titlesFor(orgId, actor, chatIds)\n : ({} as Record<string, string>),\n this.openActivities(orgId, actor, shownComments),\n this.names(\n orgId,\n [...shownComments, ...shownMessages].map((item) => item.body),\n ),\n ]);\n const titles: Record<string, ItemTitle> = {};\n for (const id of idsOf(refs, 'post')) {\n const title = posts[id];\n if (title !== undefined) {\n titles[`post:${id}`] = { title };\n }\n }\n for (const id of idsOf(refs, 'chat')) {\n const title = chats[id];\n if (title !== undefined) {\n titles[`chat:${id}`] = { title };\n }\n }\n for (const comment of shownComments) {\n const title = comment.postId\n ? posts[comment.postId]\n : comment.activityId\n ? activities[comment.activityId]\n : undefined;\n if (title !== undefined) {\n titles[`comment:${comment.id}`] = {\n title,\n excerpt: excerpt(comment.body, names, COMMENT_EXCERPT),\n };\n }\n }\n for (const message of shownMessages) {\n const title = chats[message.chatId];\n if (title !== undefined) {\n titles[`message:${message.id}`] = {\n title,\n excerpt: excerpt(message.body, names, MESSAGE_EXCERPT),\n };\n }\n }\n return titles;\n }\n\n private async found<T>(ids: string[], find: (id: string) => Promise<T | null>): Promise<T[]> {\n const rows: (T | null)[] = await Promise.all(ids.map(find));\n return rows.filter((row): row is T => row !== null);\n }\n\n private seesComment(actor: Actor, comment: Comment): boolean {\n if (comment.status === 'published' || canModerate(actor.role)) {\n return true;\n }\n return comment.orgUserId === actor.id && comment.status === 'pending';\n }\n\n private async openActivities(\n orgId: string,\n actor: Actor,\n comments: Comment[],\n ): Promise<Record<string, string>> {\n const ids = [\n ...new Set(comments.flatMap((comment) => (comment.postId ? [] : (comment.activityId ?? [])))),\n ];\n const open = await Promise.all(\n ids.map(async (id): Promise<[string, string] | null> => {\n const activity = await this.deps.activities.getActivity(orgId, id);\n if (!activity) {\n return null;\n }\n const opens =\n isStaffRole(actor.role) ||\n (await this.deps.access.hasAccess(orgId, actor.id, activity.courseId));\n return opens ? [id, activityTitle(activity)] : null;\n }),\n );\n return Object.fromEntries(open.filter((entry): entry is [string, string] => entry !== null));\n }\n\n private async names(orgId: string, bodies: string[]): Promise<Record<string, string>> {\n const ids = mentionTokens(...bodies);\n if (ids.length === 0) {\n return {};\n }\n const records = await this.deps.profiles.authorsOf(orgId, ids);\n return Object.fromEntries(\n Object.entries(records).map(([id, record]) => [\n id,\n authorView(orgId, record, null).displayName || 'A member',\n ]),\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -27,18 +27,18 @@ declare const entitlementEvents: {
|
|
|
27
27
|
createdAt: unknown;
|
|
28
28
|
updatedAt: unknown;
|
|
29
29
|
}, import("zod").ZodObject<{
|
|
30
|
-
orgId: import("zod").ZodString;
|
|
31
30
|
id: import("zod").ZodString;
|
|
31
|
+
orgId: import("zod").ZodString;
|
|
32
|
+
orgUserId: import("zod").ZodString;
|
|
33
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
34
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
32
35
|
source: import("zod").ZodString;
|
|
33
36
|
status: import("zod").ZodEnum<{
|
|
34
37
|
active: "active";
|
|
35
38
|
revoked: "revoked";
|
|
36
39
|
}>;
|
|
37
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
38
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
39
40
|
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
40
41
|
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
41
|
-
orgUserId: import("zod").ZodString;
|
|
42
42
|
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
43
43
|
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
44
44
|
}, import("zod/v4/core").$strip>>;
|
|
@@ -67,18 +67,18 @@ declare const entitlementEvents: {
|
|
|
67
67
|
createdAt: unknown;
|
|
68
68
|
updatedAt: unknown;
|
|
69
69
|
}, import("zod").ZodObject<{
|
|
70
|
-
orgId: import("zod").ZodString;
|
|
71
70
|
id: import("zod").ZodString;
|
|
71
|
+
orgId: import("zod").ZodString;
|
|
72
|
+
orgUserId: import("zod").ZodString;
|
|
73
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
74
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
72
75
|
source: import("zod").ZodString;
|
|
73
76
|
status: import("zod").ZodEnum<{
|
|
74
77
|
active: "active";
|
|
75
78
|
revoked: "revoked";
|
|
76
79
|
}>;
|
|
77
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
78
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
79
80
|
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
80
81
|
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
81
|
-
orgUserId: import("zod").ZodString;
|
|
82
82
|
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
83
83
|
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
84
84
|
}, import("zod/v4/core").$strip>>;
|
|
@@ -107,18 +107,18 @@ declare const entitlementEvents: {
|
|
|
107
107
|
createdAt: unknown;
|
|
108
108
|
updatedAt: unknown;
|
|
109
109
|
}, import("zod").ZodObject<{
|
|
110
|
-
orgId: import("zod").ZodString;
|
|
111
110
|
id: import("zod").ZodString;
|
|
111
|
+
orgId: import("zod").ZodString;
|
|
112
|
+
orgUserId: import("zod").ZodString;
|
|
113
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
114
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
112
115
|
source: import("zod").ZodString;
|
|
113
116
|
status: import("zod").ZodEnum<{
|
|
114
117
|
active: "active";
|
|
115
118
|
revoked: "revoked";
|
|
116
119
|
}>;
|
|
117
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
118
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
119
120
|
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
120
121
|
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
121
|
-
orgUserId: import("zod").ZodString;
|
|
122
122
|
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
123
123
|
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
124
124
|
}, import("zod/v4/core").$strip>>;
|
|
@@ -147,18 +147,18 @@ declare const entitlementEvents: {
|
|
|
147
147
|
createdAt: unknown;
|
|
148
148
|
updatedAt: unknown;
|
|
149
149
|
}, import("zod").ZodObject<{
|
|
150
|
-
orgId: import("zod").ZodString;
|
|
151
150
|
id: import("zod").ZodString;
|
|
151
|
+
orgId: import("zod").ZodString;
|
|
152
|
+
orgUserId: import("zod").ZodString;
|
|
153
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
154
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
152
155
|
source: import("zod").ZodString;
|
|
153
156
|
status: import("zod").ZodEnum<{
|
|
154
157
|
active: "active";
|
|
155
158
|
revoked: "revoked";
|
|
156
159
|
}>;
|
|
157
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
158
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
159
160
|
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
160
161
|
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
161
|
-
orgUserId: import("zod").ZodString;
|
|
162
162
|
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
163
163
|
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
164
164
|
}, import("zod/v4/core").$strip>>;
|
|
@@ -189,18 +189,18 @@ declare const entitlementEvents: {
|
|
|
189
189
|
updatedAt: unknown;
|
|
190
190
|
communityId: string;
|
|
191
191
|
}, import("zod").ZodObject<{
|
|
192
|
-
orgId: import("zod").ZodString;
|
|
193
192
|
id: import("zod").ZodString;
|
|
193
|
+
orgId: import("zod").ZodString;
|
|
194
|
+
orgUserId: import("zod").ZodString;
|
|
195
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
196
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
194
197
|
source: import("zod").ZodString;
|
|
195
198
|
status: import("zod").ZodEnum<{
|
|
196
199
|
active: "active";
|
|
197
200
|
revoked: "revoked";
|
|
198
201
|
}>;
|
|
199
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
200
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
201
202
|
bundleId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
202
203
|
contentId: import("zod").ZodNullable<import("zod").ZodString>;
|
|
203
|
-
orgUserId: import("zod").ZodString;
|
|
204
204
|
grantedAt: import("zod").ZodCoercedDate<unknown>;
|
|
205
205
|
expiresAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
206
206
|
communityId: import("zod").ZodString;
|