@headless-lms/adapter-db 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.
Files changed (73) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +6 -1
  3. package/dist/repositories/admin.js +1 -1
  4. package/dist/repositories/assets.d.ts +1 -2
  5. package/dist/repositories/assets.js +0 -4
  6. package/dist/repositories/assets.js.map +1 -1
  7. package/dist/repositories/automations.d.ts +0 -1
  8. package/dist/repositories/automations.js +1 -17
  9. package/dist/repositories/automations.js.map +1 -1
  10. package/dist/repositories/chats.d.ts +4 -4
  11. package/dist/repositories/chats.js +33 -18
  12. package/dist/repositories/chats.js.map +1 -1
  13. package/dist/repositories/content.d.ts +3 -3
  14. package/dist/repositories/content.js +35 -12
  15. package/dist/repositories/content.js.map +1 -1
  16. package/dist/repositories/devices.d.ts +17 -0
  17. package/dist/repositories/devices.js +75 -0
  18. package/dist/repositories/devices.js.map +1 -0
  19. package/dist/repositories/digest-runs.d.ts +14 -0
  20. package/dist/repositories/digest-runs.js +39 -0
  21. package/dist/repositories/digest-runs.js.map +1 -0
  22. package/dist/repositories/discussion.d.ts +6 -1
  23. package/dist/repositories/discussion.js +9 -4
  24. package/dist/repositories/discussion.js.map +1 -1
  25. package/dist/repositories/entitlements.d.ts +3 -1
  26. package/dist/repositories/entitlements.js +12 -6
  27. package/dist/repositories/entitlements.js.map +1 -1
  28. package/dist/repositories/inserted-row.d.ts +5 -0
  29. package/dist/repositories/inserted-row.js +7 -0
  30. package/dist/repositories/inserted-row.js.map +1 -0
  31. package/dist/repositories/mentions.d.ts +17 -0
  32. package/dist/repositories/mentions.js +55 -0
  33. package/dist/repositories/mentions.js.map +1 -0
  34. package/dist/repositories/notification-settings.d.ts +19 -0
  35. package/dist/repositories/notification-settings.js +67 -0
  36. package/dist/repositories/notification-settings.js.map +1 -0
  37. package/dist/repositories/notifications.d.ts +23 -16
  38. package/dist/repositories/notifications.js +152 -44
  39. package/dist/repositories/notifications.js.map +1 -1
  40. package/dist/repositories/organizations.d.ts +1 -0
  41. package/dist/repositories/organizations.js +4 -1
  42. package/dist/repositories/organizations.js.map +1 -1
  43. package/dist/repositories/posts.d.ts +4 -8
  44. package/dist/repositories/posts.js +25 -28
  45. package/dist/repositories/posts.js.map +1 -1
  46. package/dist/repositories/subscriptions.d.ts +18 -0
  47. package/dist/repositories/subscriptions.js +72 -0
  48. package/dist/repositories/subscriptions.js.map +1 -0
  49. package/dist/schema/automations.d.ts +524 -19
  50. package/dist/schema/automations.js +73 -7
  51. package/dist/schema/automations.js.map +1 -1
  52. package/dist/schema/discussion.d.ts +277 -127
  53. package/dist/schema/discussion.js +59 -24
  54. package/dist/schema/discussion.js.map +1 -1
  55. package/dist/schema/identity.d.ts +109 -109
  56. package/dist/schema/identity.js +28 -70
  57. package/dist/schema/identity.js.map +1 -1
  58. package/dist/schema/index.d.ts +5 -5
  59. package/dist/schema/index.js +10 -6
  60. package/drizzle/0011_digest_runs.sql +8 -0
  61. package/drizzle/0012_subscriptions.sql +16 -0
  62. package/drizzle/0013_mentions.sql +10 -0
  63. package/drizzle/0014_notification_settings_devices.sql +36 -0
  64. package/drizzle/0015_automations_drop_source.sql +2 -0
  65. package/drizzle/0016_notifications_automations.sql +48 -0
  66. package/drizzle/meta/0011_snapshot.json +8868 -0
  67. package/drizzle/meta/0012_snapshot.json +8873 -0
  68. package/drizzle/meta/0013_snapshot.json +8943 -0
  69. package/drizzle/meta/0014_snapshot.json +9305 -0
  70. package/drizzle/meta/0015_snapshot.json +9270 -0
  71. package/drizzle/meta/0016_snapshot.json +9230 -0
  72. package/drizzle/meta/_journal.json +42 -0
  73. package/package.json +2 -2
@@ -1,11 +1,12 @@
1
1
  import { activities } from "../schema/content.js";
2
- import { commentFlags, commentReactions, comments, posts } from "../schema/discussion.js";
3
2
  import { userProfiles, users } from "../schema/identity.js";
4
3
  import { orgUsers } from "../schema/organizations.js";
4
+ import { commentFlags, commentReactions, comments, posts } from "../schema/discussion.js";
5
5
  import "../schema/index.js";
6
6
  import { translateDbErrors } from "./pg-errors.js";
7
7
  import { user } from "../schema/better-auth.js";
8
8
  import { authorProfileColumns } from "./org-user-profile.js";
9
+ import { insertedRow } from "./inserted-row.js";
9
10
  import { and, asc, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm";
10
11
  import { noopLogger } from "@headless-lms/core/shared/logger";
11
12
  //#region src/repositories/discussion.ts
@@ -64,6 +65,9 @@ var DrizzleDiscussionRepository = class {
64
65
  const [row] = await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), eq(comments.id, id))).limit(1);
65
66
  return row ? toComment(row) : null;
66
67
  }
68
+ async commenterIds(orgId, target) {
69
+ return (await this.db.selectDistinct({ orgUserId: comments.orgUserId }).from(comments).where(and(eq(comments.orgId, orgId), "postId" in target ? eq(comments.postId, target.postId) : eq(comments.activityId, target.activityId), eq(comments.status, "published"))).orderBy(asc(comments.orgUserId))).map((row) => row.orgUserId);
70
+ }
67
71
  async updateComment(orgId, id, patch) {
68
72
  const [row] = await this.db.update(comments).set({
69
73
  ..."body" in patch ? { body: patch.body } : {},
@@ -113,9 +117,9 @@ var DrizzleDiscussionRepository = class {
113
117
  async setReaction(orgId, commentId, orgUserId, emoji) {
114
118
  if (emoji === null) {
115
119
  await this.db.delete(commentReactions).where(and(eq(commentReactions.orgId, orgId), eq(commentReactions.commentId, commentId), eq(commentReactions.orgUserId, orgUserId)));
116
- return;
120
+ return false;
117
121
  }
118
- await this.db.insert(commentReactions).values({
122
+ const [row] = await this.db.insert(commentReactions).values({
119
123
  orgId,
120
124
  commentId,
121
125
  orgUserId,
@@ -127,7 +131,8 @@ var DrizzleDiscussionRepository = class {
127
131
  commentReactions.orgUserId
128
132
  ],
129
133
  set: { emoji }
130
- });
134
+ }).returning({ inserted: insertedRow });
135
+ return row?.inserted === true;
131
136
  }
132
137
  async authorsOf(orgId, orgUserIds) {
133
138
  if (orgUserIds.length === 0) return {};
@@ -1 +1 @@
1
- {"version":3,"file":"discussion.js","names":[],"sources":["../../src/repositories/discussion.ts"],"sourcesContent":["import { and, asc, desc, eq, ilike, inArray, isNull, ne, or, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n AuthorRecord,\n CommentReactions,\n CommentWithContext,\n DiscussionRepository,\n PostThreadsQuery,\n} from \"@headless-lms/core/discussion\";\nimport type {\n Comment,\n CommentReport,\n FlaggedComment,\n ListCommentsQuery,\n Logger,\n Page,\n ReactionEmoji,\n Role,\n} from \"@headless-lms/core/types\";\nimport { commentFlags, commentReactions, comments, posts } from \"../schema/index.js\";\nimport { activities } from \"../schema/content.js\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport { authorProfileColumns } from \"./org-user-profile.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype CommentRow = typeof comments.$inferSelect;\n\n/** The title lives in the activity's opaque settings blob; the moderation card\n * needs it to say \"Lesson 3\" rather than an id. */\nconst activityTitleExpr = sql<string>`coalesce(${activities.settings} ->> 'title', '')`;\n\n/** Sortable columns of the staff comment list, by the client-facing field name.\n * `activityTitle` sorts on the same expression the row is served from. */\nconst sortColumns = {\n createdAt: comments.createdAt,\n updatedAt: comments.updatedAt,\n status: comments.status,\n activityTitle: activityTitleExpr,\n} as const;\n\nfunction toComment(row: CommentRow): Comment {\n return {\n id: row.id,\n orgId: row.orgId,\n activityId: row.activityId,\n postId: row.postId,\n communityId: row.communityId,\n parentId: row.parentId ?? null,\n orgUserId: row.orgUserId,\n body: row.body,\n status: row.status,\n removedBy: row.removedBy ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleDiscussionRepository implements DiscussionRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insertComment(orgId: string, comment: Comment): Promise<Comment> {\n const [row] = await this.db\n .insert(comments)\n .values({\n id: comment.id,\n orgId,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n orgUserId: comment.orgUserId,\n body: comment.body,\n status: comment.status,\n removedBy: comment.removedBy,\n createdAt: new Date(comment.createdAt),\n updatedAt: new Date(comment.updatedAt),\n })\n .returning();\n return toComment(row!);\n }\n\n async findComment(orgId: string, id: string): Promise<Comment | null> {\n const [row] = await this.db\n .select()\n .from(comments)\n .where(and(eq(comments.orgId, orgId), eq(comments.id, id)))\n .limit(1);\n return row ? toComment(row) : null;\n }\n\n async updateComment(\n orgId: string,\n id: string,\n patch: Partial<Pick<Comment, \"body\" | \"status\" | \"removedBy\" | \"updatedAt\">>,\n ): Promise<Comment | null> {\n const [row] = await this.db\n .update(comments)\n .set({\n ...(\"body\" in patch ? { body: patch.body } : {}),\n ...(\"status\" in patch ? { status: patch.status } : {}),\n ...(\"removedBy\" in patch ? { removedBy: patch.removedBy ?? null } : {}),\n })\n .where(and(eq(comments.orgId, orgId), eq(comments.id, id)))\n .returning();\n return row ? toComment(row) : null;\n }\n\n async listByActivity(orgId: string, activityId: string): Promise<Comment[]> {\n const rows = await this.db\n .select()\n .from(comments)\n .where(and(eq(comments.orgId, orgId), eq(comments.activityId, activityId)))\n .orderBy(comments.createdAt);\n return rows.map(toComment);\n }\n\n async listThreadsByPost(\n orgId: string,\n postId: string,\n query: PostThreadsQuery,\n ): Promise<Page<Comment>> {\n const rootReadable = query.seesHeld\n ? undefined\n : or(ne(comments.status, \"pending\"), eq(comments.orgUserId, query.viewerId));\n const replyReadable = query.seesHeld\n ? sql``\n : sql` and (rep.status = 'published' or rep.org_user_id = ${query.viewerId})`;\n const where = and(\n eq(comments.orgId, orgId),\n eq(comments.postId, postId),\n isNull(comments.parentId),\n rootReadable,\n or(\n ne(comments.status, \"removed\"),\n sql`exists (\n select 1 from comments rep\n where rep.org_id = ${comments.orgId}\n and rep.parent_id = ${comments.id}\n and rep.status <> 'removed'${replyReadable}\n )`,\n ),\n );\n const roots = await this.db\n .select()\n .from(comments)\n .where(where)\n .orderBy(asc(comments.createdAt), asc(comments.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .where(where);\n const replies =\n roots.length === 0\n ? []\n : await this.db\n .select()\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n inArray(\n comments.parentId,\n roots.map((root) => root.id),\n ),\n ),\n )\n .orderBy(asc(comments.createdAt), asc(comments.id));\n return {\n rows: [...roots, ...replies].map(toComment),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async reactionsOf(\n orgId: string,\n commentIds: string[],\n viewerId: string,\n ): Promise<Record<string, CommentReactions>> {\n if (commentIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({\n commentId: commentReactions.commentId,\n emoji: commentReactions.emoji,\n count: sql<number>`cast(count(*) as int)`,\n mine: sql<boolean>`bool_or(${commentReactions.orgUserId} = ${viewerId})`,\n })\n .from(commentReactions)\n .where(\n and(eq(commentReactions.orgId, orgId), inArray(commentReactions.commentId, commentIds)),\n )\n .groupBy(commentReactions.commentId, commentReactions.emoji);\n\n const byComment: Record<string, CommentReactions> = {};\n for (const row of rows) {\n const entry = (byComment[row.commentId] ??= { reactions: {} });\n entry.reactions[row.emoji] = row.count;\n if (row.mine) {\n entry.viewerReaction = row.emoji;\n }\n }\n return byComment;\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<void> {\n if (emoji === null) {\n await this.db\n .delete(commentReactions)\n .where(\n and(\n eq(commentReactions.orgId, orgId),\n eq(commentReactions.commentId, commentId),\n eq(commentReactions.orgUserId, orgUserId),\n ),\n );\n return;\n }\n await this.db\n .insert(commentReactions)\n .values({ orgId, commentId, orgUserId, emoji })\n .onConflictDoUpdate({\n target: [commentReactions.orgId, commentReactions.commentId, commentReactions.orgUserId],\n set: { emoji },\n });\n }\n\n async authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>> {\n if (orgUserIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select(authorProfileColumns)\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n\n const byId: Record<string, AuthorRecord> = {};\n for (const row of rows) {\n byId[row.id] = {\n id: row.id,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email,\n image: row.image ?? null,\n role: row.role as Role,\n displayName: row.displayName ?? null,\n headline: row.headline ?? null,\n imageAssetOrgId: row.imageAssetOrgId ?? null,\n imageAssetId: row.imageAssetId ?? null,\n };\n }\n return byId;\n }\n\n async insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null> {\n const [row] = await this.db\n .insert(commentFlags)\n .values({\n id: report.id,\n orgId,\n commentId: report.commentId,\n orgUserId: report.orgUserId,\n reason: report.reason,\n resolvedAt: report.resolvedAt ? new Date(report.resolvedAt) : null,\n resolution: report.resolution,\n resolvedBy: report.resolvedBy,\n createdAt: new Date(report.createdAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ?? null;\n }\n\n async listOpenFlagsForSpace(\n orgId: string,\n spaceId: string,\n limit: number,\n ): Promise<FlaggedComment[]> {\n const rows = await this.db\n .select({\n flag: commentFlags,\n comment: {\n id: comments.id,\n postId: posts.id,\n body: comments.body,\n orgUserId: comments.orgUserId,\n status: comments.status,\n },\n })\n .from(commentFlags)\n .innerJoin(\n comments,\n and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId)),\n )\n .innerJoin(posts, and(eq(posts.orgId, comments.orgId), eq(posts.id, comments.postId)))\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n eq(posts.spaceId, spaceId),\n isNull(commentFlags.resolvedAt),\n ),\n )\n .orderBy(asc(commentFlags.createdAt), asc(commentFlags.id))\n .limit(limit);\n return rows.map((row) => ({ ...row.flag, comment: row.comment }));\n }\n\n async publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>> {\n if (postIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({ postId: comments.postId, count: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n inArray(comments.postId, postIds),\n eq(comments.status, \"published\"),\n ),\n )\n .groupBy(comments.postId);\n const byPost: Record<string, number> = {};\n for (const row of rows) {\n if (row.postId !== null) {\n byPost[row.postId] = row.count;\n }\n }\n return byPost;\n }\n\n async listOpenReports(orgId: string, commentIds: string[]): Promise<CommentReport[]> {\n if (commentIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(commentFlags)\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n inArray(commentFlags.commentId, commentIds),\n isNull(commentFlags.resolvedAt),\n ),\n )\n .orderBy(commentFlags.createdAt);\n return rows;\n }\n\n async resolveReportsFor(\n orgId: string,\n commentId: string,\n resolved: {\n resolvedAt: Date;\n resolvedBy: string;\n resolution: \"dismissed\" | \"removed\";\n },\n ): Promise<void> {\n await this.db\n .update(commentFlags)\n .set({\n resolvedAt: resolved.resolvedAt,\n resolvedBy: resolved.resolvedBy,\n resolution: resolved.resolution,\n })\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n eq(commentFlags.commentId, commentId),\n isNull(commentFlags.resolvedAt),\n ),\n );\n }\n\n /** The list's one join: comments to their activity, which carries both the\n * course (denormalised, so the module never enters the query) and the title.\n * The course filter and the row's activity title come out of that same pass,\n * so neither costs an extra query. */\n private listRows(where: SQL | undefined) {\n return this.db\n .select({\n comment: comments,\n courseId: activities.courseId,\n activityTitle: activityTitleExpr,\n })\n .from(comments)\n .innerJoin(\n activities,\n and(eq(activities.orgId, comments.orgId), eq(activities.id, comments.activityId)),\n )\n .where(where);\n }\n\n /** The same join and filters, counted. Separate from `listRows` because the\n * total must ignore limit/offset. */\n private listTotal(where: SQL | undefined) {\n return this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .innerJoin(\n activities,\n and(eq(activities.orgId, comments.orgId), eq(activities.id, comments.activityId)),\n )\n .where(where);\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>> {\n const conditions: SQL[] = [eq(comments.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(comments.status, query.status));\n }\n if (query.courseId) {\n conditions.push(eq(activities.courseId, query.courseId));\n }\n if (query.activityId) {\n conditions.push(eq(comments.activityId, query.activityId));\n }\n if (query.orgUserId) {\n conditions.push(eq(comments.orgUserId, query.orgUserId));\n }\n if (query.reported !== undefined) {\n // Correlated EXISTS rather than a join to comment_flags: a comment with\n // three open reports must stay one row.\n const hasOpenReport = sql`exists (\n select 1 from ${commentFlags} r\n where r.org_id = ${comments.orgId}\n and r.comment_id = ${comments.id}\n and r.resolved_at is null\n )`;\n conditions.push(query.reported ? hasOpenReport : sql`not ${hasOpenReport}`);\n }\n if (query.search) {\n conditions.push(ilike(comments.body, `%${query.search}%`));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to newest first, which is the\n // order a moderator works in.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? comments.createdAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(comments.createdAt);\n }\n\n const rows = await this.listRows(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [{ total } = { total: 0 }] = await this.listTotal(where);\n\n return {\n rows: rows.map((r) => ({\n comment: toComment(r.comment),\n courseId: r.courseId,\n activityTitle: r.activityTitle,\n })),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\ntranslateDbErrors(DrizzleDiscussionRepository);\n"],"mappings":";;;;;;;;;;;;;AAgCA,MAAM,oBAAoB,GAAW,YAAY,WAAW,SAAS;;;AAIrE,MAAM,cAAc;CAClB,WAAW,SAAS;CACpB,WAAW,SAAS;CACpB,QAAQ,SAAS;CACjB,eAAe;AACjB;AAEA,SAAS,UAAU,KAA0B;CAC3C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,YAAY,IAAI;EAChB,QAAQ,IAAI;EACZ,aAAa,IAAI;EACjB,UAAU,IAAI,YAAY;EAC1B,WAAW,IAAI;EACf,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,WAAW,IAAI,aAAa;EAC5B,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,8BAAb,MAAyE;CAEpD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,cAAc,OAAe,SAAoC;EACrE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;GACN,IAAI,QAAQ;GACZ;GACA,YAAY,QAAQ;GACpB,QAAQ,QAAQ;GAChB,aAAa,QAAQ;GACrB,UAAU,QAAQ;GAClB,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,QAAQ,QAAQ;GAChB,WAAW,QAAQ;GACnB,WAAW,IAAI,KAAK,QAAQ,SAAS;GACrC,WAAW,IAAI,KAAK,QAAQ,SAAS;EACvC,CAAC,CAAC,CACD,UAAU;EACb,OAAO,UAAU,GAAI;CACvB;CAEA,MAAM,YAAY,OAAe,IAAqC;EACpE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAC1D,MAAM,CAAC;EACV,OAAO,MAAM,UAAU,GAAG,IAAI;CAChC;CAEA,MAAM,cACJ,OACA,IACA,OACyB;EACzB,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,IAAI;GACH,GAAI,UAAU,QAAQ,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;GAC9C,GAAI,YAAY,QAAQ,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;GACpD,GAAI,eAAe,QAAQ,EAAE,WAAW,MAAM,aAAa,KAAK,IAAI,CAAC;EACvE,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAC1D,UAAU;EACb,OAAO,MAAM,UAAU,GAAG,IAAI;CAChC;CAEA,MAAM,eAAe,OAAe,YAAwC;EAM1E,QAAO,MALY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,YAAY,UAAU,CAAC,CAAC,CAAC,CAC1E,QAAQ,SAAS,SAAS,EAAA,CACjB,IAAI,SAAS;CAC3B;CAEA,MAAM,kBACJ,OACA,QACA,OACwB;EACxB,MAAM,eAAe,MAAM,WACvB,KAAA,IACA,GAAG,GAAG,SAAS,QAAQ,SAAS,GAAG,GAAG,SAAS,WAAW,MAAM,QAAQ,CAAC;EAC7E,MAAM,gBAAgB,MAAM,WACxB,GAAG,KACH,GAAG,uDAAuD,MAAM,SAAS;EAC7E,MAAM,QAAQ,IACZ,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,QAAQ,MAAM,GAC1B,OAAO,SAAS,QAAQ,GACxB,cACA,GACE,GAAG,SAAS,QAAQ,SAAS,GAC7B,GAAG;;+BAEoB,SAAS,MAAM;kCACZ,SAAS,GAAG;yCACL,cAAc;UAEjD,CACF;EACA,MAAM,QAAQ,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MAAM,KAAK,CAAC,CACZ,QAAQ,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC,CAAC,CAClD,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ;EAC3C,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,MAAM,KAAK,GAC3C,OAAO,EAAE,OAAO,GAAW,wBAAwB,CAAC,CAAC,CACrD,KAAK,QAAQ,CAAC,CACd,MAAM,KAAK;EACd,MAAM,UACJ,MAAM,WAAW,IACb,CAAC,IACD,MAAM,KAAK,GACR,OAAO,CAAC,CACR,KAAK,QAAQ,CAAC,CACd,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,QACE,SAAS,UACT,MAAM,KAAK,SAAS,KAAK,EAAE,CAC7B,CACF,CACF,CAAC,CACA,QAAQ,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;EAC1D,OAAO;GACL,MAAM,CAAC,GAAG,OAAO,GAAG,OAAO,CAAC,CAAC,IAAI,SAAS;GAC1C;GACA,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;CAEA,MAAM,YACJ,OACA,YACA,UAC2C;EAC3C,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GACN,WAAW,iBAAiB;GAC5B,OAAO,iBAAiB;GACxB,OAAO,GAAW;GAClB,MAAM,GAAY,WAAW,iBAAiB,UAAU,KAAK,SAAS;EACxE,CAAC,CAAC,CACD,KAAK,gBAAgB,CAAC,CACtB,MACC,IAAI,GAAG,iBAAiB,OAAO,KAAK,GAAG,QAAQ,iBAAiB,WAAW,UAAU,CAAC,CACxF,CAAC,CACA,QAAQ,iBAAiB,WAAW,iBAAiB,KAAK;EAE7D,MAAM,YAA8C,CAAC;EACrD,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAS,UAAU,IAAI,eAAe,EAAE,WAAW,CAAC,EAAE;GAC5D,MAAM,UAAU,IAAI,SAAS,IAAI;GACjC,IAAI,IAAI,MACN,MAAM,iBAAiB,IAAI;EAE/B;EACA,OAAO;CACT;CAEA,MAAM,YACJ,OACA,WACA,WACA,OACe;EACf,IAAI,UAAU,MAAM;GAClB,MAAM,KAAK,GACR,OAAO,gBAAgB,CAAC,CACxB,MACC,IACE,GAAG,iBAAiB,OAAO,KAAK,GAChC,GAAG,iBAAiB,WAAW,SAAS,GACxC,GAAG,iBAAiB,WAAW,SAAS,CAC1C,CACF;GACF;EACF;EACA,MAAM,KAAK,GACR,OAAO,gBAAgB,CAAC,CACxB,OAAO;GAAE;GAAO;GAAW;GAAW;EAAM,CAAC,CAAC,CAC9C,mBAAmB;GAClB,QAAQ;IAAC,iBAAiB;IAAO,iBAAiB;IAAW,iBAAiB;GAAS;GACvF,KAAK,EAAE,MAAM;EACf,CAAC;CACL;CAEA,MAAM,UAAU,OAAe,YAA6D;EAC1F,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO,oBAAoB,CAAC,CAC5B,KAAK,QAAQ,CAAC,CACd,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,SAAS,MAAM,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC,CAAC,CAC7C,SAAS,cAAc,GAAG,aAAa,QAAQ,SAAS,MAAM,CAAC,CAAC,CAChE,MAAM,IAAI,GAAG,SAAS,OAAO,KAAK,GAAG,QAAQ,SAAS,IAAI,UAAU,CAAC,CAAC;EAEzE,MAAM,OAAqC,CAAC;EAC5C,KAAK,MAAM,OAAO,MAChB,KAAK,IAAI,MAAM;GACb,IAAI,IAAI;GACR,WAAW,IAAI;GACf,UAAU,IAAI;GACd,OAAO,IAAI;GACX,OAAO,IAAI,SAAS;GACpB,MAAM,IAAI;GACV,aAAa,IAAI,eAAe;GAChC,UAAU,IAAI,YAAY;GAC1B,iBAAiB,IAAI,mBAAmB;GACxC,cAAc,IAAI,gBAAgB;EACpC;EAEF,OAAO;CACT;CAEA,MAAM,aAAa,OAAe,QAAsD;EACtF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,YAAY,CAAC,CACpB,OAAO;GACN,IAAI,OAAO;GACX;GACA,WAAW,OAAO;GAClB,WAAW,OAAO;GAClB,QAAQ,OAAO;GACf,YAAY,OAAO,aAAa,IAAI,KAAK,OAAO,UAAU,IAAI;GAC9D,YAAY,OAAO;GACnB,YAAY,OAAO;GACnB,WAAW,IAAI,KAAK,OAAO,SAAS;EACtC,CAAC,CAAC,CACD,oBAAoB,CAAC,CACrB,UAAU;EACb,OAAO,OAAO;CAChB;CAEA,MAAM,sBACJ,OACA,SACA,OAC2B;EA2B3B,QAAO,MA1BY,KAAK,GACrB,OAAO;GACN,MAAM;GACN,SAAS;IACP,IAAI,SAAS;IACb,QAAQ,MAAM;IACd,MAAM,SAAS;IACf,WAAW,SAAS;IACpB,QAAQ,SAAS;GACnB;EACF,CAAC,CAAC,CACD,KAAK,YAAY,CAAC,CAClB,UACC,UACA,IAAI,GAAG,SAAS,OAAO,aAAa,KAAK,GAAG,GAAG,SAAS,IAAI,aAAa,SAAS,CAAC,CACrF,CAAC,CACA,UAAU,OAAO,IAAI,GAAG,MAAM,OAAO,SAAS,KAAK,GAAG,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC,CACrF,MACC,IACE,GAAG,aAAa,OAAO,KAAK,GAC5B,GAAG,MAAM,SAAS,OAAO,GACzB,OAAO,aAAa,UAAU,CAChC,CACF,CAAC,CACA,QAAQ,IAAI,aAAa,SAAS,GAAG,IAAI,aAAa,EAAE,CAAC,CAAC,CAC1D,MAAM,KAAK,EAAA,CACF,KAAK,SAAS;GAAE,GAAG,IAAI;GAAM,SAAS,IAAI;EAAQ,EAAE;CAClE;CAEA,MAAM,uBAAuB,OAAe,SAAoD;EAC9F,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GAAE,QAAQ,SAAS;GAAQ,OAAO,GAAW;EAAwB,CAAC,CAAC,CAC9E,KAAK,QAAQ,CAAC,CACd,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,QAAQ,SAAS,QAAQ,OAAO,GAChC,GAAG,SAAS,QAAQ,WAAW,CACjC,CACF,CAAC,CACA,QAAQ,SAAS,MAAM;EAC1B,MAAM,SAAiC,CAAC;EACxC,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,WAAW,MACjB,OAAO,IAAI,UAAU,IAAI;EAG7B,OAAO;CACT;CAEA,MAAM,gBAAgB,OAAe,YAAgD;EACnF,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAaV,OAAO,MAXY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,YAAY,CAAC,CAClB,MACC,IACE,GAAG,aAAa,OAAO,KAAK,GAC5B,QAAQ,aAAa,WAAW,UAAU,GAC1C,OAAO,aAAa,UAAU,CAChC,CACF,CAAC,CACA,QAAQ,aAAa,SAAS;CAEnC;CAEA,MAAM,kBACJ,OACA,WACA,UAKe;EACf,MAAM,KAAK,GACR,OAAO,YAAY,CAAC,CACpB,IAAI;GACH,YAAY,SAAS;GACrB,YAAY,SAAS;GACrB,YAAY,SAAS;EACvB,CAAC,CAAC,CACD,MACC,IACE,GAAG,aAAa,OAAO,KAAK,GAC5B,GAAG,aAAa,WAAW,SAAS,GACpC,OAAO,aAAa,UAAU,CAChC,CACF;CACJ;;;;;CAMA,SAAiB,OAAwB;EACvC,OAAO,KAAK,GACT,OAAO;GACN,SAAS;GACT,UAAU,WAAW;GACrB,eAAe;EACjB,CAAC,CAAC,CACD,KAAK,QAAQ,CAAC,CACd,UACC,YACA,IAAI,GAAG,WAAW,OAAO,SAAS,KAAK,GAAG,GAAG,WAAW,IAAI,SAAS,UAAU,CAAC,CAClF,CAAC,CACA,MAAM,KAAK;CAChB;;;CAIA,UAAkB,OAAwB;EACxC,OAAO,KAAK,GACT,OAAO,EAAE,OAAO,GAAW,wBAAwB,CAAC,CAAC,CACrD,KAAK,QAAQ,CAAC,CACd,UACC,YACA,IAAI,GAAG,WAAW,OAAO,SAAS,KAAK,GAAG,GAAG,WAAW,IAAI,SAAS,UAAU,CAAC,CAClF,CAAC,CACA,MAAM,KAAK;CAChB;CAEA,MAAM,aAAa,OAAe,OAA6D;EAC7F,MAAM,aAAoB,CAAC,GAAG,SAAS,OAAO,KAAK,CAAC;EACpD,IAAI,MAAM,QACR,WAAW,KAAK,GAAG,SAAS,QAAQ,MAAM,MAAM,CAAC;EAEnD,IAAI,MAAM,UACR,WAAW,KAAK,GAAG,WAAW,UAAU,MAAM,QAAQ,CAAC;EAEzD,IAAI,MAAM,YACR,WAAW,KAAK,GAAG,SAAS,YAAY,MAAM,UAAU,CAAC;EAE3D,IAAI,MAAM,WACR,WAAW,KAAK,GAAG,SAAS,WAAW,MAAM,SAAS,CAAC;EAEzD,IAAI,MAAM,aAAa,KAAA,GAAW;GAGhC,MAAM,gBAAgB,GAAG;wBACP,aAAa;2BACV,SAAS,MAAM;+BACX,SAAS,GAAG;;;GAGrC,WAAW,KAAK,MAAM,WAAW,gBAAgB,GAAG,OAAO,eAAe;EAC5E;EACA,IAAI,MAAM,QACR,WAAW,KAAK,MAAM,SAAS,MAAM,IAAI,MAAM,OAAO,EAAE,CAAC;EAE3D,MAAM,QAAQ,IAAI,GAAG,UAAU;EAI/B,IAAI;EACJ,IAAI,MAAM,MAAM;GACd,MAAM,SAAS,MAAM,KAAK,WAAW,GAAG;GACxC,MAAM,QAAS,SAAS,MAAM,KAAK,MAAM,CAAC,IAAI,MAAM;GACpD,MAAM,MAAM,YAAY,UAAU,SAAS;GAC3C,UAAU,SAAS,KAAK,GAAG,IAAI,IAAI,GAAG;EACxC,OACE,UAAU,KAAK,SAAS,SAAS;EAGnC,MAAM,OAAO,MAAM,KAAK,SAAS,KAAK,CAAC,CACpC,QAAQ,OAAO,CAAC,CAChB,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ;EAE3C,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,MAAM,KAAK,UAAU,KAAK;EAE7D,OAAO;GACL,MAAM,KAAK,KAAK,OAAO;IACrB,SAAS,UAAU,EAAE,OAAO;IAC5B,UAAU,EAAE;IACZ,eAAe,EAAE;GACnB,EAAE;GACF;GACA,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;AACF;AACA,kBAAkB,2BAA2B"}
1
+ {"version":3,"file":"discussion.js","names":[],"sources":["../../src/repositories/discussion.ts"],"sourcesContent":["import { and, asc, desc, eq, ilike, inArray, isNull, ne, or, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n AuthorRecord,\n CommentReactions,\n CommentWithContext,\n DiscussionRepository,\n PostThreadsQuery,\n} from \"@headless-lms/core/discussion\";\nimport type {\n Comment,\n CommentReport,\n FlaggedComment,\n ListCommentsQuery,\n Logger,\n Page,\n ReactionEmoji,\n Role,\n} from \"@headless-lms/core/types\";\nimport { commentFlags, commentReactions, comments, posts } from \"../schema/index.js\";\nimport { activities } from \"../schema/content.js\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport { authorProfileColumns } from \"./org-user-profile.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { insertedRow } from \"./inserted-row.js\";\n\ntype CommentRow = typeof comments.$inferSelect;\n\n/** The title lives in the activity's opaque settings blob; the moderation card\n * needs it to say \"Lesson 3\" rather than an id. */\nconst activityTitleExpr = sql<string>`coalesce(${activities.settings} ->> 'title', '')`;\n\n/** Sortable columns of the staff comment list, by the client-facing field name.\n * `activityTitle` sorts on the same expression the row is served from. */\nconst sortColumns = {\n createdAt: comments.createdAt,\n updatedAt: comments.updatedAt,\n status: comments.status,\n activityTitle: activityTitleExpr,\n} as const;\n\nfunction toComment(row: CommentRow): Comment {\n return {\n id: row.id,\n orgId: row.orgId,\n activityId: row.activityId,\n postId: row.postId,\n communityId: row.communityId,\n parentId: row.parentId ?? null,\n orgUserId: row.orgUserId,\n body: row.body,\n status: row.status,\n removedBy: row.removedBy ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleDiscussionRepository implements DiscussionRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insertComment(orgId: string, comment: Comment): Promise<Comment> {\n const [row] = await this.db\n .insert(comments)\n .values({\n id: comment.id,\n orgId,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n orgUserId: comment.orgUserId,\n body: comment.body,\n status: comment.status,\n removedBy: comment.removedBy,\n createdAt: new Date(comment.createdAt),\n updatedAt: new Date(comment.updatedAt),\n })\n .returning();\n return toComment(row!);\n }\n\n async findComment(orgId: string, id: string): Promise<Comment | null> {\n const [row] = await this.db\n .select()\n .from(comments)\n .where(and(eq(comments.orgId, orgId), eq(comments.id, id)))\n .limit(1);\n return row ? toComment(row) : null;\n }\n\n async commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ orgUserId: comments.orgUserId })\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n \"postId\" in target\n ? eq(comments.postId, target.postId)\n : eq(comments.activityId, target.activityId),\n eq(comments.status, \"published\"),\n ),\n )\n .orderBy(asc(comments.orgUserId));\n return rows.map((row) => row.orgUserId);\n }\n\n async updateComment(\n orgId: string,\n id: string,\n patch: Partial<Pick<Comment, \"body\" | \"status\" | \"removedBy\" | \"updatedAt\">>,\n ): Promise<Comment | null> {\n const [row] = await this.db\n .update(comments)\n .set({\n ...(\"body\" in patch ? { body: patch.body } : {}),\n ...(\"status\" in patch ? { status: patch.status } : {}),\n ...(\"removedBy\" in patch ? { removedBy: patch.removedBy ?? null } : {}),\n })\n .where(and(eq(comments.orgId, orgId), eq(comments.id, id)))\n .returning();\n return row ? toComment(row) : null;\n }\n\n async listByActivity(orgId: string, activityId: string): Promise<Comment[]> {\n const rows = await this.db\n .select()\n .from(comments)\n .where(and(eq(comments.orgId, orgId), eq(comments.activityId, activityId)))\n .orderBy(comments.createdAt);\n return rows.map(toComment);\n }\n\n async listThreadsByPost(\n orgId: string,\n postId: string,\n query: PostThreadsQuery,\n ): Promise<Page<Comment>> {\n const rootReadable = query.seesHeld\n ? undefined\n : or(ne(comments.status, \"pending\"), eq(comments.orgUserId, query.viewerId));\n const replyReadable = query.seesHeld\n ? sql``\n : sql` and (rep.status = 'published' or rep.org_user_id = ${query.viewerId})`;\n const where = and(\n eq(comments.orgId, orgId),\n eq(comments.postId, postId),\n isNull(comments.parentId),\n rootReadable,\n or(\n ne(comments.status, \"removed\"),\n sql`exists (\n select 1 from comments rep\n where rep.org_id = ${comments.orgId}\n and rep.parent_id = ${comments.id}\n and rep.status <> 'removed'${replyReadable}\n )`,\n ),\n );\n const roots = await this.db\n .select()\n .from(comments)\n .where(where)\n .orderBy(asc(comments.createdAt), asc(comments.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .where(where);\n const replies =\n roots.length === 0\n ? []\n : await this.db\n .select()\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n inArray(\n comments.parentId,\n roots.map((root) => root.id),\n ),\n ),\n )\n .orderBy(asc(comments.createdAt), asc(comments.id));\n return {\n rows: [...roots, ...replies].map(toComment),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async reactionsOf(\n orgId: string,\n commentIds: string[],\n viewerId: string,\n ): Promise<Record<string, CommentReactions>> {\n if (commentIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({\n commentId: commentReactions.commentId,\n emoji: commentReactions.emoji,\n count: sql<number>`cast(count(*) as int)`,\n mine: sql<boolean>`bool_or(${commentReactions.orgUserId} = ${viewerId})`,\n })\n .from(commentReactions)\n .where(\n and(eq(commentReactions.orgId, orgId), inArray(commentReactions.commentId, commentIds)),\n )\n .groupBy(commentReactions.commentId, commentReactions.emoji);\n\n const byComment: Record<string, CommentReactions> = {};\n for (const row of rows) {\n const entry = (byComment[row.commentId] ??= { reactions: {} });\n entry.reactions[row.emoji] = row.count;\n if (row.mine) {\n entry.viewerReaction = row.emoji;\n }\n }\n return byComment;\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<boolean> {\n if (emoji === null) {\n await this.db\n .delete(commentReactions)\n .where(\n and(\n eq(commentReactions.orgId, orgId),\n eq(commentReactions.commentId, commentId),\n eq(commentReactions.orgUserId, orgUserId),\n ),\n );\n return false;\n }\n const [row] = await this.db\n .insert(commentReactions)\n .values({ orgId, commentId, orgUserId, emoji })\n .onConflictDoUpdate({\n target: [commentReactions.orgId, commentReactions.commentId, commentReactions.orgUserId],\n set: { emoji },\n })\n .returning({ inserted: insertedRow });\n return row?.inserted === true;\n }\n\n async authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>> {\n if (orgUserIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select(authorProfileColumns)\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n\n const byId: Record<string, AuthorRecord> = {};\n for (const row of rows) {\n byId[row.id] = {\n id: row.id,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email,\n image: row.image ?? null,\n role: row.role as Role,\n displayName: row.displayName ?? null,\n headline: row.headline ?? null,\n imageAssetOrgId: row.imageAssetOrgId ?? null,\n imageAssetId: row.imageAssetId ?? null,\n };\n }\n return byId;\n }\n\n async insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null> {\n const [row] = await this.db\n .insert(commentFlags)\n .values({\n id: report.id,\n orgId,\n commentId: report.commentId,\n orgUserId: report.orgUserId,\n reason: report.reason,\n resolvedAt: report.resolvedAt ? new Date(report.resolvedAt) : null,\n resolution: report.resolution,\n resolvedBy: report.resolvedBy,\n createdAt: new Date(report.createdAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ?? null;\n }\n\n async listOpenFlagsForSpace(\n orgId: string,\n spaceId: string,\n limit: number,\n ): Promise<FlaggedComment[]> {\n const rows = await this.db\n .select({\n flag: commentFlags,\n comment: {\n id: comments.id,\n postId: posts.id,\n body: comments.body,\n orgUserId: comments.orgUserId,\n status: comments.status,\n },\n })\n .from(commentFlags)\n .innerJoin(\n comments,\n and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId)),\n )\n .innerJoin(posts, and(eq(posts.orgId, comments.orgId), eq(posts.id, comments.postId)))\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n eq(posts.spaceId, spaceId),\n isNull(commentFlags.resolvedAt),\n ),\n )\n .orderBy(asc(commentFlags.createdAt), asc(commentFlags.id))\n .limit(limit);\n return rows.map((row) => ({ ...row.flag, comment: row.comment }));\n }\n\n async publishedCommentCounts(orgId: string, postIds: string[]): Promise<Record<string, number>> {\n if (postIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({ postId: comments.postId, count: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n inArray(comments.postId, postIds),\n eq(comments.status, \"published\"),\n ),\n )\n .groupBy(comments.postId);\n const byPost: Record<string, number> = {};\n for (const row of rows) {\n if (row.postId !== null) {\n byPost[row.postId] = row.count;\n }\n }\n return byPost;\n }\n\n async listOpenReports(orgId: string, commentIds: string[]): Promise<CommentReport[]> {\n if (commentIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(commentFlags)\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n inArray(commentFlags.commentId, commentIds),\n isNull(commentFlags.resolvedAt),\n ),\n )\n .orderBy(commentFlags.createdAt);\n return rows;\n }\n\n async resolveReportsFor(\n orgId: string,\n commentId: string,\n resolved: {\n resolvedAt: Date;\n resolvedBy: string;\n resolution: \"dismissed\" | \"removed\";\n },\n ): Promise<void> {\n await this.db\n .update(commentFlags)\n .set({\n resolvedAt: resolved.resolvedAt,\n resolvedBy: resolved.resolvedBy,\n resolution: resolved.resolution,\n })\n .where(\n and(\n eq(commentFlags.orgId, orgId),\n eq(commentFlags.commentId, commentId),\n isNull(commentFlags.resolvedAt),\n ),\n );\n }\n\n /** The list's one join: comments to their activity, which carries both the\n * course (denormalised, so the module never enters the query) and the title.\n * The course filter and the row's activity title come out of that same pass,\n * so neither costs an extra query. */\n private listRows(where: SQL | undefined) {\n return this.db\n .select({\n comment: comments,\n courseId: activities.courseId,\n activityTitle: activityTitleExpr,\n })\n .from(comments)\n .innerJoin(\n activities,\n and(eq(activities.orgId, comments.orgId), eq(activities.id, comments.activityId)),\n )\n .where(where);\n }\n\n /** The same join and filters, counted. Separate from `listRows` because the\n * total must ignore limit/offset. */\n private listTotal(where: SQL | undefined) {\n return this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(comments)\n .innerJoin(\n activities,\n and(eq(activities.orgId, comments.orgId), eq(activities.id, comments.activityId)),\n )\n .where(where);\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>> {\n const conditions: SQL[] = [eq(comments.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(comments.status, query.status));\n }\n if (query.courseId) {\n conditions.push(eq(activities.courseId, query.courseId));\n }\n if (query.activityId) {\n conditions.push(eq(comments.activityId, query.activityId));\n }\n if (query.orgUserId) {\n conditions.push(eq(comments.orgUserId, query.orgUserId));\n }\n if (query.reported !== undefined) {\n // Correlated EXISTS rather than a join to comment_flags: a comment with\n // three open reports must stay one row.\n const hasOpenReport = sql`exists (\n select 1 from ${commentFlags} r\n where r.org_id = ${comments.orgId}\n and r.comment_id = ${comments.id}\n and r.resolved_at is null\n )`;\n conditions.push(query.reported ? hasOpenReport : sql`not ${hasOpenReport}`);\n }\n if (query.search) {\n conditions.push(ilike(comments.body, `%${query.search}%`));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to newest first, which is the\n // order a moderator works in.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? comments.createdAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(comments.createdAt);\n }\n\n const rows = await this.listRows(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [{ total } = { total: 0 }] = await this.listTotal(where);\n\n return {\n rows: rows.map((r) => ({\n comment: toComment(r.comment),\n courseId: r.courseId,\n activityTitle: r.activityTitle,\n })),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\ntranslateDbErrors(DrizzleDiscussionRepository);\n"],"mappings":"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"}
@@ -36,13 +36,15 @@ declare class DrizzleEntitlementsRepository implements EntitlementsRepository {
36
36
  activeSpaceIds(orgId: string, orgUserId: string, communityId: string): Promise<string[]>;
37
37
  listGrantsExpiredBy(orgId: string, now: Date, limit: number, afterId?: string): Promise<Entitlement[]>;
38
38
  listGrantsEndingBetween(orgId: string, after: Date, until: Date, limit: number, afterKey?: EndingGrantKey): Promise<EndingGrant[]>;
39
- hasDueNotices(orgId: string, now: Date, until: Date): Promise<boolean>;
39
+ orgIdsWithDueNotices(now: Date, until: Date): Promise<string[]>;
40
40
  claimNotice(orgId: string, notice: EntitlementNotice, now: Date): Promise<boolean>;
41
41
  private expiredUnnoticed;
42
42
  private endingUnnoticed;
43
43
  communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
44
44
  communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
45
45
  communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
46
+ contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
47
+ openCommunityIds(orgId: string, orgUserId: string): Promise<string[]>;
46
48
  }
47
49
  //#endregion
48
50
  export { DrizzleEntitlementsRepository };
@@ -292,10 +292,10 @@ var DrizzleEntitlementsRepository = class {
292
292
  communityId
293
293
  }));
294
294
  }
295
- async hasDueNotices(orgId, now, until) {
296
- const expired = this.db.select({ id: entitlements.id }).from(entitlements).where(this.expiredUnnoticed(orgId, now));
297
- const ending = this.db.select({ id: entitlements.id }).from(entitlements).innerJoin(communities, opensPublishedCommunity).where(this.endingUnnoticed(orgId, now, until));
298
- return this.holds(sql`(exists (${expired}) or exists (${ending}))`);
295
+ async orgIdsWithDueNotices(now, until) {
296
+ const expired = this.db.selectDistinct({ orgId: entitlements.orgId }).from(entitlements).where(this.expiredUnnoticed(null, now));
297
+ const ending = this.db.selectDistinct({ orgId: entitlements.orgId }).from(entitlements).innerJoin(communities, opensPublishedCommunity).where(this.endingUnnoticed(null, now, until));
298
+ return (await expired.union(ending)).map((row) => row.orgId).sort();
299
299
  }
300
300
  async claimNotice(orgId, notice, now) {
301
301
  const statement = notice.kind === "expired" ? sql`insert into ${entitlementExpiryNotices} ("org_id", "entitlement_id", "expires_at", "notified_at")
@@ -309,10 +309,10 @@ var DrizzleEntitlementsRepository = class {
309
309
  return (Array.isArray(result) ? result : result.rows).length > 0;
310
310
  }
311
311
  expiredUnnoticed(orgId, now) {
312
- return and(eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt, at(now)), sql`not ${expiryNoticed}`);
312
+ return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt, at(now)), sql`not ${expiryNoticed}`);
313
313
  }
314
314
  endingUnnoticed(orgId, after, until) {
315
- return and(eq(entitlements.orgId, orgId), grantIsActive(entitlements, at(after)), grantExpired(entitlements.expiresAt, at(until)), sql`not ${endingSoonNoticed}`);
315
+ return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), grantIsActive(entitlements, at(after)), grantExpired(entitlements.expiresAt, at(until)), sql`not ${endingSoonNoticed}`);
316
316
  }
317
317
  async communityModeratorIds(orgId, communityId) {
318
318
  return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.role, MODERATING_ROLES), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`))).orderBy(asc(orgUsers.id))).map((row) => row.id);
@@ -323,6 +323,12 @@ var DrizzleEntitlementsRepository = class {
323
323
  async communityIdsOpenedFor(orgId, orgUserId) {
324
324
  return (await this.db.selectDistinct({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), holdsCommunity(sql`${communities.orgId}`, sql`${orgUserId}`, sql`${communities.id}`))).orderBy(asc(communities.id))).map((row) => row.id);
325
325
  }
326
+ async contentHolderIds(orgId, contentId) {
327
+ return (await this.db.selectDistinct({ orgUserId: contentAccess.orgUserId }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, contentId))).orderBy(asc(contentAccess.orgUserId))).map((row) => row.orgUserId);
328
+ }
329
+ async openCommunityIds(orgId, orgUserId) {
330
+ return (await this.db.select({ id: communities.id }).from(communities).innerJoin(orgUsers, and(eq(orgUsers.orgId, communities.orgId), eq(orgUsers.id, orgUserId))).where(and(eq(communities.orgId, orgId), communityMember(sql`${communities.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communities.id}`))).orderBy(asc(communities.id))).map((row) => row.id);
331
+ }
326
332
  };
327
333
  function toSpaceMember(row) {
328
334
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"entitlements.js","names":[],"sources":["../../src/repositories/entitlements.ts"],"sourcesContent":["import { and, asc, desc, eq, gt, ilike, inArray, or, sql, type SQL } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type {\n AccessFacts,\n EndingGrant,\n EndingGrantKey,\n EntitlementNotice,\n EntitlementsRepository,\n GrantKey,\n GrantTarget,\n InsertSpaceMemberInput,\n SpaceMembersQuery,\n} from \"@headless-lms/core/entitlements\";\nimport type {\n ContentType,\n Entitlement,\n EntitlementStatus,\n EntitlementsQuery,\n GrantEntitlementInput,\n Logger,\n Page,\n Role,\n SpaceMember,\n SpaceMemberStatus,\n} from \"@headless-lms/core/types\";\nimport {\n contentAccess,\n entitlementEndingSoonNotices,\n entitlementExpiryNotices,\n entitlements,\n orgUsers,\n spaceMembers,\n users,\n} from \"../schema/index.js\";\nimport {\n grantExpired,\n grantIsActive,\n communityMember,\n grantOpens,\n holdsCommunity,\n} from \"./grant-covers.js\";\nimport { bundles, communities, contentItems, courses, downloads } from \"../schema/content.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ROLES, canModerate } from \"@headless-lms/core/organizations\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype ResolvedStatus = \"active\" | \"expired\" | \"revoked\";\n\nconst MODERATING_ROLES: Role[] = ROLES.filter((role) => canModerate(role));\n\n// CASE expression: revoked beats everything; otherwise an elapsed expiry reads as\n// expired; otherwise active.\nconst derivedStatus = sql<ResolvedStatus>`case\n when ${entitlements.status} = 'revoked' then 'revoked'\n when ${grantExpired(entitlements.expiresAt)} then 'expired'\n else 'active'\nend`;\n\nfunction at(date: Date): SQL {\n return sql`${date.toISOString()}::timestamptz`;\n}\n\nconst expiryNoticed = sql`exists (\n select 1 from ${entitlementExpiryNotices}\n where ${entitlementExpiryNotices.orgId} = ${entitlements.orgId}\n and ${entitlementExpiryNotices.entitlementId} = ${entitlements.id}\n and ${entitlementExpiryNotices.expiresAt} = ${entitlements.expiresAt}\n)`;\n\nconst endingSoonNoticed = sql`exists (\n select 1 from ${entitlementEndingSoonNotices}\n where ${entitlementEndingSoonNotices.orgId} = ${entitlements.orgId}\n and ${entitlementEndingSoonNotices.entitlementId} = ${entitlements.id}\n and ${entitlementEndingSoonNotices.communityId} = ${communities.id}\n and ${entitlementEndingSoonNotices.expiresAt} = ${entitlements.expiresAt}\n)`;\n\nconst opensPublishedCommunity = and(\n eq(communities.orgId, entitlements.orgId),\n eq(communities.status, \"published\"),\n grantOpens(entitlements, sql`${communities.id}`, \"community\"),\n);\n\n// Display name of the grant's target, whatever it is. One LEFT JOIN per concrete\n// content table plus one for bundles; exactly one hits per row (a grant targets\n// either a bundle or a single content item), so the COALESCE picks the single\n// non-null title. Extended per new content type.\nconst contentTitle = sql<string>`coalesce(${courses.title}, ${downloads.title}, ${communities.title}, ${bundles.name})`;\n\n// Sortable columns by the client-facing field name. `status` sorts on the derived\n// expression so the ordering matches the displayed value; `contentTitle` on the\n// coalesced join expression.\nconst sortColumns = {\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n contentTitle,\n status: derivedStatus,\n grantedAt: entitlements.grantedAt,\n expiresAt: entitlements.expiresAt,\n source: entitlements.source,\n} as const;\n\nconst selection = {\n orgId: entitlements.orgId,\n id: entitlements.id,\n orgUserId: entitlements.orgUserId,\n bundleId: entitlements.bundleId,\n contentId: entitlements.contentId,\n status: entitlements.status,\n source: entitlements.source,\n grantedAt: entitlements.grantedAt,\n expiresAt: entitlements.expiresAt,\n createdAt: entitlements.createdAt,\n updatedAt: entitlements.updatedAt,\n} as const;\n\ninterface Row {\n orgId: string;\n id: string;\n orgUserId: string;\n bundleId: string | null;\n contentId: string | null;\n status: EntitlementStatus;\n source: string;\n grantedAt: Date;\n expiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toEntitlement(row: Row): Entitlement {\n return {\n orgId: row.orgId,\n id: row.id,\n orgUserId: row.orgUserId,\n bundleId: row.bundleId,\n contentId: row.contentId,\n status: row.status,\n source: row.source,\n grantedAt: row.grantedAt,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleEntitlementsRepository implements EntitlementsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** entitlements → org_users + the grant's target: content_items (type) and one\n * LEFT JOIN per concrete content table (title), or bundles (name). Both target\n * joins are LEFT — a row carries one of the two, never both. */\n private joined(where: SQL | undefined) {\n return this.db\n .select(selection)\n .from(entitlements)\n .innerJoin(\n orgUsers,\n and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(\n contentItems,\n and(\n eq(contentItems.orgId, entitlements.orgId),\n eq(contentItems.id, entitlements.contentId),\n ),\n )\n .leftJoin(\n bundles,\n and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId)),\n )\n .leftJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .leftJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .leftJoin(\n communities,\n and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId)),\n )\n .where(where);\n }\n\n async list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n const conditions: SQL[] = [eq(entitlements.orgId, orgId)];\n if (query.status) {\n conditions.push(sql`${derivedStatus} = ${query.status}`);\n }\n if (query.source) {\n conditions.push(eq(entitlements.source, query.source));\n }\n if (query.orgUserId) {\n conditions.push(eq(entitlements.orgUserId, query.orgUserId));\n }\n if (query.contentId) {\n conditions.push(eq(entitlements.contentId, query.contentId));\n }\n if (query.bundleId) {\n conditions.push(eq(entitlements.bundleId, query.bundleId));\n }\n if (query.type) {\n conditions.push(eq(contentItems.type, query.type));\n }\n if (query.search) {\n const pattern = `%${query.search}%`;\n conditions.push(\n or(\n ilike(users.firstName, pattern),\n ilike(users.lastName, pattern),\n ilike(users.email, pattern),\n ilike(contentTitle, pattern),\n ) as SQL,\n );\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently granted first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? entitlements.grantedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(entitlements.grantedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.joined(where).orderBy(orderBy).limit(query.pageSize).offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(entitlements)\n .innerJoin(\n orgUsers,\n and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(\n contentItems,\n and(\n eq(contentItems.orgId, entitlements.orgId),\n eq(contentItems.id, entitlements.contentId),\n ),\n )\n .leftJoin(\n bundles,\n and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId)),\n )\n .leftJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .leftJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .leftJoin(\n communities,\n and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId)),\n )\n .where(where);\n\n return {\n rows: rows.map(toEntitlement),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n /** A grant targets exactly one of a bundle or a content item (db check\n * constraint), so the upsert is inferred on whichever of the two partial\n * uniques the target uses. */\n async insert(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const bundleId = input.bundleId ?? null;\n const contentId = input.contentId ?? null;\n const target = bundleId\n ? [entitlements.orgId, entitlements.orgUserId, entitlements.bundleId]\n : [entitlements.orgId, entitlements.orgUserId, entitlements.contentId];\n\n const [row] = await this.db\n .insert(entitlements)\n .values({\n orgId,\n orgUserId: input.orgUserId,\n bundleId,\n contentId,\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target,\n set: {\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n },\n })\n .returning(selection);\n if (!row) {\n throw new Error(\"failed to insert entitlement\");\n }\n return toEntitlement(row);\n }\n\n async insertIfAbsent(orgId: string, input: GrantEntitlementInput): Promise<Entitlement | null> {\n const bundleId = input.bundleId ?? null;\n const contentId = input.contentId ?? null;\n const target = bundleId\n ? [entitlements.orgId, entitlements.orgUserId, entitlements.bundleId]\n : [entitlements.orgId, entitlements.orgUserId, entitlements.contentId];\n const [row] = await this.db\n .insert(entitlements)\n .values({\n orgId,\n orgUserId: input.orgUserId,\n bundleId,\n contentId,\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n })\n .onConflictDoNothing({ target })\n .returning(selection);\n return row ? toEntitlement(row) : null;\n }\n\n async lockGrant(orgId: string, key: GrantKey): Promise<Entitlement | null> {\n const target = key.bundleId\n ? eq(entitlements.bundleId, key.bundleId)\n : key.contentId\n ? eq(entitlements.contentId, key.contentId)\n : undefined;\n if (!target) {\n return null;\n }\n const [row] = await this.db\n .select(selection)\n .from(entitlements)\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.orgUserId, key.orgUserId), target))\n .for(\"update\")\n .limit(1);\n return row ? toEntitlement(row) : null;\n }\n\n async setExpiresAt(\n orgId: string,\n id: string,\n expiresAt: Date | null,\n ): Promise<Entitlement | null> {\n const [row] = await this.db\n .update(entitlements)\n .set({ expiresAt })\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id)))\n .returning(selection);\n return row ? toEntitlement(row) : null;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: \"active\" | \"revoked\",\n ): Promise<Entitlement | null> {\n const [row] = await this.db\n .update(entitlements)\n .set({ status })\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id)))\n .returning(selection);\n if (!row) {\n return null;\n }\n return toEntitlement(row);\n }\n\n async hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean> {\n const rows = await this.db\n .select({ id: contentAccess.entitlementId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.orgUserId, orgUserId),\n eq(contentAccess.contentId, contentId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n }\n\n async listAccessibleContent(\n orgId: string,\n orgUserId: string,\n type?: ContentType,\n ): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ contentId: contentAccess.contentId })\n .from(contentAccess)\n .innerJoin(\n contentItems,\n and(\n eq(contentItems.orgId, contentAccess.orgId),\n eq(contentItems.id, contentAccess.contentId),\n ),\n )\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.orgUserId, orgUserId),\n type ? eq(contentItems.type, type) : undefined,\n ),\n );\n return rows.map((r) => r.contentId);\n }\n\n hasCommunityAccess(orgId: string, orgUserId: string, communityId: string): Promise<boolean> {\n return this.holds(holdsCommunity(sql`${orgId}`, sql`${orgUserId}`, sql`${communityId}`));\n }\n\n private async holds(predicate: SQL): Promise<boolean> {\n const result = await this.db.execute<{ covered: boolean }>(sql`select ${predicate} as covered`);\n const rows = Array.isArray(result) ? result : result.rows;\n return rows[0]?.covered === true;\n }\n\n async communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]> {\n if (!grant.contentId && !grant.bundleId) {\n return [];\n }\n const target = {\n orgId: sql`${orgId}`,\n contentId: sql`${grant.contentId ?? null}::text`,\n bundleId: sql`${grant.bundleId ?? null}::text`,\n };\n const rows = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(\n and(\n eq(communities.orgId, orgId),\n eq(communities.status, \"published\"),\n grantOpens(target, sql`${communities.id}`, \"community\"),\n ),\n )\n .orderBy(asc(communities.id));\n return rows.map((row) => row.id);\n }\n\n async accessAmong(\n orgId: string,\n communityId: string,\n spaceId: string | null,\n orgUserIds: string[],\n ): Promise<AccessFacts[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const activeInSpace = spaceId\n ? sql<boolean>`exists (\n select 1 from space_members sm\n where sm.org_id = ${sql`${orgUsers.orgId}`}\n and sm.space_id = ${spaceId}\n and sm.org_user_id = ${sql`${orgUsers.id}`}\n and sm.status = 'active'\n )`\n : sql<boolean>`false`;\n const rows = await this.db\n .select({\n orgUserId: orgUsers.id,\n role: orgUsers.role,\n member: sql<boolean>`${communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`)}`,\n activeInSpace,\n })\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n return rows.map((row) => ({\n orgUserId: row.orgUserId,\n role: row.role as Role,\n member: row.member === true,\n activeInSpace: row.activeInSpace === true,\n }));\n }\n\n async findSpaceMember(\n orgId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.orgUserId, orgUserId),\n ),\n )\n .limit(1);\n return row ? toSpaceMember(row) : null;\n }\n\n async listSpaceMembersFor(\n orgId: string,\n orgUserId: string,\n communityId: string,\n ): Promise<SpaceMember[]> {\n const rows = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.orgUserId, orgUserId),\n eq(spaceMembers.communityId, communityId),\n ),\n );\n return rows.map(toSpaceMember);\n }\n\n async lockSpaceMember(\n orgId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.orgUserId, orgUserId),\n ),\n )\n .for(\"update\")\n .limit(1);\n return row ? toSpaceMember(row) : null;\n }\n\n async listSpaceMembers(\n orgId: string,\n spaceId: string,\n query: SpaceMembersQuery,\n ): Promise<Page<SpaceMember>> {\n const where = and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n query.statuses ? inArray(spaceMembers.status, query.statuses) : undefined,\n );\n const rows = await this.db\n .select()\n .from(spaceMembers)\n .where(where)\n .orderBy(asc(spaceMembers.createdAt), asc(spaceMembers.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(spaceMembers)\n .where(where);\n return { rows: rows.map(toSpaceMember), total, page: query.page, pageSize: query.pageSize };\n }\n\n async insertSpaceMember(orgId: string, input: InsertSpaceMemberInput): Promise<SpaceMember> {\n const [row] = await this.db\n .insert(spaceMembers)\n .values({\n orgId,\n communityId: input.communityId,\n spaceId: input.spaceId,\n orgUserId: input.orgUserId,\n status: input.status,\n source: input.source,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert space member\");\n }\n return toSpaceMember(row);\n }\n\n async updateSpaceMemberStatus(\n orgId: string,\n id: string,\n status: SpaceMemberStatus,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .update(spaceMembers)\n .set({ status })\n .where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)))\n .returning();\n return row ? toSpaceMember(row) : null;\n }\n\n async deleteSpaceMember(orgId: string, id: string): Promise<void> {\n await this.db\n .delete(spaceMembers)\n .where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)));\n }\n\n async countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number> {\n const [row] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.status, \"active\"),\n holdsCommunity(\n sql`${spaceMembers.orgId}`,\n sql`${spaceMembers.orgUserId}`,\n sql`${spaceMembers.communityId}`,\n ),\n ),\n );\n return row?.total ?? 0;\n }\n\n async activeSpaceIds(orgId: string, orgUserId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ spaceId: spaceMembers.spaceId })\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.orgUserId, orgUserId),\n eq(spaceMembers.communityId, communityId),\n eq(spaceMembers.status, \"active\"),\n ),\n );\n return rows.map((row) => row.spaceId);\n }\n\n async listGrantsExpiredBy(\n orgId: string,\n now: Date,\n limit: number,\n afterId?: string,\n ): Promise<Entitlement[]> {\n const rows = await this.db\n .select(selection)\n .from(entitlements)\n .where(\n and(\n this.expiredUnnoticed(orgId, now),\n afterId === undefined ? undefined : gt(entitlements.id, afterId),\n ),\n )\n .orderBy(asc(entitlements.id))\n .limit(limit);\n return rows.map(toEntitlement);\n }\n\n async listGrantsEndingBetween(\n orgId: string,\n after: Date,\n until: Date,\n limit: number,\n afterKey?: EndingGrantKey,\n ): Promise<EndingGrant[]> {\n const rows = await this.db\n .select({ ...selection, communityId: communities.id })\n .from(entitlements)\n .innerJoin(communities, opensPublishedCommunity)\n .where(\n and(\n this.endingUnnoticed(orgId, after, until),\n afterKey\n ? sql`(${entitlements.id}, ${communities.id}) > (${afterKey.entitlementId}, ${afterKey.communityId})`\n : undefined,\n ),\n )\n .orderBy(asc(entitlements.id), asc(communities.id))\n .limit(limit);\n return rows.map(({ communityId, ...grant }) => ({\n grant: toEntitlement(grant),\n communityId,\n }));\n }\n\n async hasDueNotices(orgId: string, now: Date, until: Date): Promise<boolean> {\n const expired = this.db\n .select({ id: entitlements.id })\n .from(entitlements)\n .where(this.expiredUnnoticed(orgId, now));\n const ending = this.db\n .select({ id: entitlements.id })\n .from(entitlements)\n .innerJoin(communities, opensPublishedCommunity)\n .where(this.endingUnnoticed(orgId, now, until));\n return this.holds(sql`(exists (${expired}) or exists (${ending}))`);\n }\n\n async claimNotice(orgId: string, notice: EntitlementNotice, now: Date): Promise<boolean> {\n const statement =\n notice.kind === \"expired\"\n ? sql`insert into ${entitlementExpiryNotices} (\"org_id\", \"entitlement_id\", \"expires_at\", \"notified_at\")\n values (${orgId}, ${notice.entitlementId}, ${at(notice.expiresAt)}, ${at(now)})\n on conflict do nothing\n returning \"entitlement_id\"`\n : sql`insert into ${entitlementEndingSoonNotices} (\"org_id\", \"entitlement_id\", \"community_id\", \"expires_at\", \"notified_at\")\n values (${orgId}, ${notice.entitlementId}, ${notice.communityId}, ${at(notice.expiresAt)}, ${at(now)})\n on conflict do nothing\n returning \"entitlement_id\"`;\n const result = await this.db.execute<{ entitlement_id: string }>(statement);\n const rows = Array.isArray(result) ? result : result.rows;\n return rows.length > 0;\n }\n\n private expiredUnnoticed(orgId: string, now: Date): SQL {\n return and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.status, \"active\"),\n grantExpired(entitlements.expiresAt, at(now)),\n sql`not ${expiryNoticed}`,\n )!;\n }\n\n private endingUnnoticed(orgId: string, after: Date, until: Date): SQL {\n return and(\n eq(entitlements.orgId, orgId),\n grantIsActive(entitlements, at(after)),\n grantExpired(entitlements.expiresAt, at(until)),\n sql`not ${endingSoonNoticed}`,\n )!;\n }\n\n async communityModeratorIds(orgId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n inArray(orgUsers.role, MODERATING_ROLES),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n ),\n )\n .orderBy(asc(orgUsers.id));\n return rows.map((row) => row.id);\n }\n\n async communityMemberIds(orgId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n ),\n )\n .orderBy(asc(orgUsers.id));\n return rows.map((row) => row.id);\n }\n\n async communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ id: communities.id })\n .from(communities)\n .where(\n and(\n eq(communities.orgId, orgId),\n holdsCommunity(sql`${communities.orgId}`, sql`${orgUserId}`, sql`${communities.id}`),\n ),\n )\n .orderBy(asc(communities.id));\n return rows.map((row) => row.id);\n }\n}\n\nfunction toSpaceMember(row: {\n orgId: string;\n id: string;\n communityId: string;\n spaceId: string;\n orgUserId: string;\n status: SpaceMemberStatus;\n source: \"request\" | \"invite\" | \"manual\";\n createdAt: Date;\n updatedAt: Date;\n}): SpaceMember {\n return {\n orgId: row.orgId,\n id: row.id,\n communityId: row.communityId,\n spaceId: row.spaceId,\n orgUserId: row.orgUserId,\n status: row.status,\n source: row.source,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\ntranslateDbErrors(DrizzleEntitlementsRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"entitlements.js","names":[],"sources":["../../src/repositories/entitlements.ts"],"sourcesContent":["import { and, asc, desc, eq, gt, ilike, inArray, or, sql, type SQL } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type {\n AccessFacts,\n EndingGrant,\n EndingGrantKey,\n EntitlementNotice,\n EntitlementsRepository,\n GrantKey,\n GrantTarget,\n InsertSpaceMemberInput,\n SpaceMembersQuery,\n} from \"@headless-lms/core/entitlements\";\nimport type {\n ContentType,\n Entitlement,\n EntitlementStatus,\n EntitlementsQuery,\n GrantEntitlementInput,\n Logger,\n Page,\n Role,\n SpaceMember,\n SpaceMemberStatus,\n} from \"@headless-lms/core/types\";\nimport {\n contentAccess,\n entitlementEndingSoonNotices,\n entitlementExpiryNotices,\n entitlements,\n orgUsers,\n spaceMembers,\n users,\n} from \"../schema/index.js\";\nimport {\n grantExpired,\n grantIsActive,\n communityMember,\n grantOpens,\n holdsCommunity,\n} from \"./grant-covers.js\";\nimport { bundles, communities, contentItems, courses, downloads } from \"../schema/content.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ROLES, canModerate } from \"@headless-lms/core/organizations\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype ResolvedStatus = \"active\" | \"expired\" | \"revoked\";\n\nconst MODERATING_ROLES: Role[] = ROLES.filter((role) => canModerate(role));\n\n// CASE expression: revoked beats everything; otherwise an elapsed expiry reads as\n// expired; otherwise active.\nconst derivedStatus = sql<ResolvedStatus>`case\n when ${entitlements.status} = 'revoked' then 'revoked'\n when ${grantExpired(entitlements.expiresAt)} then 'expired'\n else 'active'\nend`;\n\nfunction at(date: Date): SQL {\n return sql`${date.toISOString()}::timestamptz`;\n}\n\nconst expiryNoticed = sql`exists (\n select 1 from ${entitlementExpiryNotices}\n where ${entitlementExpiryNotices.orgId} = ${entitlements.orgId}\n and ${entitlementExpiryNotices.entitlementId} = ${entitlements.id}\n and ${entitlementExpiryNotices.expiresAt} = ${entitlements.expiresAt}\n)`;\n\nconst endingSoonNoticed = sql`exists (\n select 1 from ${entitlementEndingSoonNotices}\n where ${entitlementEndingSoonNotices.orgId} = ${entitlements.orgId}\n and ${entitlementEndingSoonNotices.entitlementId} = ${entitlements.id}\n and ${entitlementEndingSoonNotices.communityId} = ${communities.id}\n and ${entitlementEndingSoonNotices.expiresAt} = ${entitlements.expiresAt}\n)`;\n\nconst opensPublishedCommunity = and(\n eq(communities.orgId, entitlements.orgId),\n eq(communities.status, \"published\"),\n grantOpens(entitlements, sql`${communities.id}`, \"community\"),\n);\n\n// Display name of the grant's target, whatever it is. One LEFT JOIN per concrete\n// content table plus one for bundles; exactly one hits per row (a grant targets\n// either a bundle or a single content item), so the COALESCE picks the single\n// non-null title. Extended per new content type.\nconst contentTitle = sql<string>`coalesce(${courses.title}, ${downloads.title}, ${communities.title}, ${bundles.name})`;\n\n// Sortable columns by the client-facing field name. `status` sorts on the derived\n// expression so the ordering matches the displayed value; `contentTitle` on the\n// coalesced join expression.\nconst sortColumns = {\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n contentTitle,\n status: derivedStatus,\n grantedAt: entitlements.grantedAt,\n expiresAt: entitlements.expiresAt,\n source: entitlements.source,\n} as const;\n\nconst selection = {\n orgId: entitlements.orgId,\n id: entitlements.id,\n orgUserId: entitlements.orgUserId,\n bundleId: entitlements.bundleId,\n contentId: entitlements.contentId,\n status: entitlements.status,\n source: entitlements.source,\n grantedAt: entitlements.grantedAt,\n expiresAt: entitlements.expiresAt,\n createdAt: entitlements.createdAt,\n updatedAt: entitlements.updatedAt,\n} as const;\n\ninterface Row {\n orgId: string;\n id: string;\n orgUserId: string;\n bundleId: string | null;\n contentId: string | null;\n status: EntitlementStatus;\n source: string;\n grantedAt: Date;\n expiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toEntitlement(row: Row): Entitlement {\n return {\n orgId: row.orgId,\n id: row.id,\n orgUserId: row.orgUserId,\n bundleId: row.bundleId,\n contentId: row.contentId,\n status: row.status,\n source: row.source,\n grantedAt: row.grantedAt,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleEntitlementsRepository implements EntitlementsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** entitlements → org_users + the grant's target: content_items (type) and one\n * LEFT JOIN per concrete content table (title), or bundles (name). Both target\n * joins are LEFT — a row carries one of the two, never both. */\n private joined(where: SQL | undefined) {\n return this.db\n .select(selection)\n .from(entitlements)\n .innerJoin(\n orgUsers,\n and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(\n contentItems,\n and(\n eq(contentItems.orgId, entitlements.orgId),\n eq(contentItems.id, entitlements.contentId),\n ),\n )\n .leftJoin(\n bundles,\n and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId)),\n )\n .leftJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .leftJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .leftJoin(\n communities,\n and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId)),\n )\n .where(where);\n }\n\n async list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n const conditions: SQL[] = [eq(entitlements.orgId, orgId)];\n if (query.status) {\n conditions.push(sql`${derivedStatus} = ${query.status}`);\n }\n if (query.source) {\n conditions.push(eq(entitlements.source, query.source));\n }\n if (query.orgUserId) {\n conditions.push(eq(entitlements.orgUserId, query.orgUserId));\n }\n if (query.contentId) {\n conditions.push(eq(entitlements.contentId, query.contentId));\n }\n if (query.bundleId) {\n conditions.push(eq(entitlements.bundleId, query.bundleId));\n }\n if (query.type) {\n conditions.push(eq(contentItems.type, query.type));\n }\n if (query.search) {\n const pattern = `%${query.search}%`;\n conditions.push(\n or(\n ilike(users.firstName, pattern),\n ilike(users.lastName, pattern),\n ilike(users.email, pattern),\n ilike(contentTitle, pattern),\n ) as SQL,\n );\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently granted first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? entitlements.grantedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(entitlements.grantedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.joined(where).orderBy(orderBy).limit(query.pageSize).offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(entitlements)\n .innerJoin(\n orgUsers,\n and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(\n contentItems,\n and(\n eq(contentItems.orgId, entitlements.orgId),\n eq(contentItems.id, entitlements.contentId),\n ),\n )\n .leftJoin(\n bundles,\n and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId)),\n )\n .leftJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .leftJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .leftJoin(\n communities,\n and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId)),\n )\n .where(where);\n\n return {\n rows: rows.map(toEntitlement),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n /** A grant targets exactly one of a bundle or a content item (db check\n * constraint), so the upsert is inferred on whichever of the two partial\n * uniques the target uses. */\n async insert(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const bundleId = input.bundleId ?? null;\n const contentId = input.contentId ?? null;\n const target = bundleId\n ? [entitlements.orgId, entitlements.orgUserId, entitlements.bundleId]\n : [entitlements.orgId, entitlements.orgUserId, entitlements.contentId];\n\n const [row] = await this.db\n .insert(entitlements)\n .values({\n orgId,\n orgUserId: input.orgUserId,\n bundleId,\n contentId,\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target,\n set: {\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n },\n })\n .returning(selection);\n if (!row) {\n throw new Error(\"failed to insert entitlement\");\n }\n return toEntitlement(row);\n }\n\n async insertIfAbsent(orgId: string, input: GrantEntitlementInput): Promise<Entitlement | null> {\n const bundleId = input.bundleId ?? null;\n const contentId = input.contentId ?? null;\n const target = bundleId\n ? [entitlements.orgId, entitlements.orgUserId, entitlements.bundleId]\n : [entitlements.orgId, entitlements.orgUserId, entitlements.contentId];\n const [row] = await this.db\n .insert(entitlements)\n .values({\n orgId,\n orgUserId: input.orgUserId,\n bundleId,\n contentId,\n status: \"active\",\n source: \"manual\",\n grantedAt: new Date(),\n expiresAt: input.expiresAt,\n })\n .onConflictDoNothing({ target })\n .returning(selection);\n return row ? toEntitlement(row) : null;\n }\n\n async lockGrant(orgId: string, key: GrantKey): Promise<Entitlement | null> {\n const target = key.bundleId\n ? eq(entitlements.bundleId, key.bundleId)\n : key.contentId\n ? eq(entitlements.contentId, key.contentId)\n : undefined;\n if (!target) {\n return null;\n }\n const [row] = await this.db\n .select(selection)\n .from(entitlements)\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.orgUserId, key.orgUserId), target))\n .for(\"update\")\n .limit(1);\n return row ? toEntitlement(row) : null;\n }\n\n async setExpiresAt(\n orgId: string,\n id: string,\n expiresAt: Date | null,\n ): Promise<Entitlement | null> {\n const [row] = await this.db\n .update(entitlements)\n .set({ expiresAt })\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id)))\n .returning(selection);\n return row ? toEntitlement(row) : null;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: \"active\" | \"revoked\",\n ): Promise<Entitlement | null> {\n const [row] = await this.db\n .update(entitlements)\n .set({ status })\n .where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id)))\n .returning(selection);\n if (!row) {\n return null;\n }\n return toEntitlement(row);\n }\n\n async hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean> {\n const rows = await this.db\n .select({ id: contentAccess.entitlementId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.orgUserId, orgUserId),\n eq(contentAccess.contentId, contentId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n }\n\n async listAccessibleContent(\n orgId: string,\n orgUserId: string,\n type?: ContentType,\n ): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ contentId: contentAccess.contentId })\n .from(contentAccess)\n .innerJoin(\n contentItems,\n and(\n eq(contentItems.orgId, contentAccess.orgId),\n eq(contentItems.id, contentAccess.contentId),\n ),\n )\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.orgUserId, orgUserId),\n type ? eq(contentItems.type, type) : undefined,\n ),\n );\n return rows.map((r) => r.contentId);\n }\n\n hasCommunityAccess(orgId: string, orgUserId: string, communityId: string): Promise<boolean> {\n return this.holds(holdsCommunity(sql`${orgId}`, sql`${orgUserId}`, sql`${communityId}`));\n }\n\n private async holds(predicate: SQL): Promise<boolean> {\n const result = await this.db.execute<{ covered: boolean }>(sql`select ${predicate} as covered`);\n const rows = Array.isArray(result) ? result : result.rows;\n return rows[0]?.covered === true;\n }\n\n async communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]> {\n if (!grant.contentId && !grant.bundleId) {\n return [];\n }\n const target = {\n orgId: sql`${orgId}`,\n contentId: sql`${grant.contentId ?? null}::text`,\n bundleId: sql`${grant.bundleId ?? null}::text`,\n };\n const rows = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(\n and(\n eq(communities.orgId, orgId),\n eq(communities.status, \"published\"),\n grantOpens(target, sql`${communities.id}`, \"community\"),\n ),\n )\n .orderBy(asc(communities.id));\n return rows.map((row) => row.id);\n }\n\n async accessAmong(\n orgId: string,\n communityId: string,\n spaceId: string | null,\n orgUserIds: string[],\n ): Promise<AccessFacts[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const activeInSpace = spaceId\n ? sql<boolean>`exists (\n select 1 from space_members sm\n where sm.org_id = ${sql`${orgUsers.orgId}`}\n and sm.space_id = ${spaceId}\n and sm.org_user_id = ${sql`${orgUsers.id}`}\n and sm.status = 'active'\n )`\n : sql<boolean>`false`;\n const rows = await this.db\n .select({\n orgUserId: orgUsers.id,\n role: orgUsers.role,\n member: sql<boolean>`${communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`)}`,\n activeInSpace,\n })\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n return rows.map((row) => ({\n orgUserId: row.orgUserId,\n role: row.role as Role,\n member: row.member === true,\n activeInSpace: row.activeInSpace === true,\n }));\n }\n\n async findSpaceMember(\n orgId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.orgUserId, orgUserId),\n ),\n )\n .limit(1);\n return row ? toSpaceMember(row) : null;\n }\n\n async listSpaceMembersFor(\n orgId: string,\n orgUserId: string,\n communityId: string,\n ): Promise<SpaceMember[]> {\n const rows = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.orgUserId, orgUserId),\n eq(spaceMembers.communityId, communityId),\n ),\n );\n return rows.map(toSpaceMember);\n }\n\n async lockSpaceMember(\n orgId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .select()\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.orgUserId, orgUserId),\n ),\n )\n .for(\"update\")\n .limit(1);\n return row ? toSpaceMember(row) : null;\n }\n\n async listSpaceMembers(\n orgId: string,\n spaceId: string,\n query: SpaceMembersQuery,\n ): Promise<Page<SpaceMember>> {\n const where = and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n query.statuses ? inArray(spaceMembers.status, query.statuses) : undefined,\n );\n const rows = await this.db\n .select()\n .from(spaceMembers)\n .where(where)\n .orderBy(asc(spaceMembers.createdAt), asc(spaceMembers.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(spaceMembers)\n .where(where);\n return { rows: rows.map(toSpaceMember), total, page: query.page, pageSize: query.pageSize };\n }\n\n async insertSpaceMember(orgId: string, input: InsertSpaceMemberInput): Promise<SpaceMember> {\n const [row] = await this.db\n .insert(spaceMembers)\n .values({\n orgId,\n communityId: input.communityId,\n spaceId: input.spaceId,\n orgUserId: input.orgUserId,\n status: input.status,\n source: input.source,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert space member\");\n }\n return toSpaceMember(row);\n }\n\n async updateSpaceMemberStatus(\n orgId: string,\n id: string,\n status: SpaceMemberStatus,\n ): Promise<SpaceMember | null> {\n const [row] = await this.db\n .update(spaceMembers)\n .set({ status })\n .where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)))\n .returning();\n return row ? toSpaceMember(row) : null;\n }\n\n async deleteSpaceMember(orgId: string, id: string): Promise<void> {\n await this.db\n .delete(spaceMembers)\n .where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)));\n }\n\n async countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number> {\n const [row] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.spaceId, spaceId),\n eq(spaceMembers.status, \"active\"),\n holdsCommunity(\n sql`${spaceMembers.orgId}`,\n sql`${spaceMembers.orgUserId}`,\n sql`${spaceMembers.communityId}`,\n ),\n ),\n );\n return row?.total ?? 0;\n }\n\n async activeSpaceIds(orgId: string, orgUserId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ spaceId: spaceMembers.spaceId })\n .from(spaceMembers)\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.orgUserId, orgUserId),\n eq(spaceMembers.communityId, communityId),\n eq(spaceMembers.status, \"active\"),\n ),\n );\n return rows.map((row) => row.spaceId);\n }\n\n async listGrantsExpiredBy(\n orgId: string,\n now: Date,\n limit: number,\n afterId?: string,\n ): Promise<Entitlement[]> {\n const rows = await this.db\n .select(selection)\n .from(entitlements)\n .where(\n and(\n this.expiredUnnoticed(orgId, now),\n afterId === undefined ? undefined : gt(entitlements.id, afterId),\n ),\n )\n .orderBy(asc(entitlements.id))\n .limit(limit);\n return rows.map(toEntitlement);\n }\n\n async listGrantsEndingBetween(\n orgId: string,\n after: Date,\n until: Date,\n limit: number,\n afterKey?: EndingGrantKey,\n ): Promise<EndingGrant[]> {\n const rows = await this.db\n .select({ ...selection, communityId: communities.id })\n .from(entitlements)\n .innerJoin(communities, opensPublishedCommunity)\n .where(\n and(\n this.endingUnnoticed(orgId, after, until),\n afterKey\n ? sql`(${entitlements.id}, ${communities.id}) > (${afterKey.entitlementId}, ${afterKey.communityId})`\n : undefined,\n ),\n )\n .orderBy(asc(entitlements.id), asc(communities.id))\n .limit(limit);\n return rows.map(({ communityId, ...grant }) => ({\n grant: toEntitlement(grant),\n communityId,\n }));\n }\n\n async orgIdsWithDueNotices(now: Date, until: Date): Promise<string[]> {\n const expired = this.db\n .selectDistinct({ orgId: entitlements.orgId })\n .from(entitlements)\n .where(this.expiredUnnoticed(null, now));\n const ending = this.db\n .selectDistinct({ orgId: entitlements.orgId })\n .from(entitlements)\n .innerJoin(communities, opensPublishedCommunity)\n .where(this.endingUnnoticed(null, now, until));\n const rows = await expired.union(ending);\n return rows.map((row) => row.orgId).sort();\n }\n\n async claimNotice(orgId: string, notice: EntitlementNotice, now: Date): Promise<boolean> {\n const statement =\n notice.kind === \"expired\"\n ? sql`insert into ${entitlementExpiryNotices} (\"org_id\", \"entitlement_id\", \"expires_at\", \"notified_at\")\n values (${orgId}, ${notice.entitlementId}, ${at(notice.expiresAt)}, ${at(now)})\n on conflict do nothing\n returning \"entitlement_id\"`\n : sql`insert into ${entitlementEndingSoonNotices} (\"org_id\", \"entitlement_id\", \"community_id\", \"expires_at\", \"notified_at\")\n values (${orgId}, ${notice.entitlementId}, ${notice.communityId}, ${at(notice.expiresAt)}, ${at(now)})\n on conflict do nothing\n returning \"entitlement_id\"`;\n const result = await this.db.execute<{ entitlement_id: string }>(statement);\n const rows = Array.isArray(result) ? result : result.rows;\n return rows.length > 0;\n }\n\n private expiredUnnoticed(orgId: string | null, now: Date): SQL {\n return and(\n orgId === null ? undefined : eq(entitlements.orgId, orgId),\n eq(entitlements.status, \"active\"),\n grantExpired(entitlements.expiresAt, at(now)),\n sql`not ${expiryNoticed}`,\n )!;\n }\n\n private endingUnnoticed(orgId: string | null, after: Date, until: Date): SQL {\n return and(\n orgId === null ? undefined : eq(entitlements.orgId, orgId),\n grantIsActive(entitlements, at(after)),\n grantExpired(entitlements.expiresAt, at(until)),\n sql`not ${endingSoonNoticed}`,\n )!;\n }\n\n async communityModeratorIds(orgId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n inArray(orgUsers.role, MODERATING_ROLES),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n ),\n )\n .orderBy(asc(orgUsers.id));\n return rows.map((row) => row.id);\n }\n\n async communityMemberIds(orgId: string, communityId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n ),\n )\n .orderBy(asc(orgUsers.id));\n return rows.map((row) => row.id);\n }\n\n async communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ id: communities.id })\n .from(communities)\n .where(\n and(\n eq(communities.orgId, orgId),\n holdsCommunity(sql`${communities.orgId}`, sql`${orgUserId}`, sql`${communities.id}`),\n ),\n )\n .orderBy(asc(communities.id));\n return rows.map((row) => row.id);\n }\n\n async contentHolderIds(orgId: string, contentId: string): Promise<string[]> {\n const rows = await this.db\n .selectDistinct({ orgUserId: contentAccess.orgUserId })\n .from(contentAccess)\n .where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, contentId)))\n .orderBy(asc(contentAccess.orgUserId));\n return rows.map((row) => row.orgUserId);\n }\n\n async openCommunityIds(orgId: string, orgUserId: string): Promise<string[]> {\n const rows = await this.db\n .select({ id: communities.id })\n .from(communities)\n .innerJoin(orgUsers, and(eq(orgUsers.orgId, communities.orgId), eq(orgUsers.id, orgUserId)))\n .where(\n and(\n eq(communities.orgId, orgId),\n communityMember(\n sql`${communities.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communities.id}`,\n ),\n ),\n )\n .orderBy(asc(communities.id));\n return rows.map((row) => row.id);\n }\n}\n\nfunction toSpaceMember(row: {\n orgId: string;\n id: string;\n communityId: string;\n spaceId: string;\n orgUserId: string;\n status: SpaceMemberStatus;\n source: \"request\" | \"invite\" | \"manual\";\n createdAt: Date;\n updatedAt: Date;\n}): SpaceMember {\n return {\n orgId: row.orgId,\n id: row.id,\n communityId: row.communityId,\n spaceId: row.spaceId,\n orgUserId: row.orgUserId,\n status: row.status,\n source: row.source,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\ntranslateDbErrors(DrizzleEntitlementsRepository);\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ //#region src/repositories/inserted-row.d.ts
2
+ declare const insertedRow: import("drizzle-orm").SQL<boolean>;
3
+ //#endregion
4
+ export { insertedRow };
5
+ //# sourceMappingURL=inserted-row.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { sql } from "drizzle-orm";
2
+ //#region src/repositories/inserted-row.ts
3
+ const insertedRow = sql`(xmax = 0)`;
4
+ //#endregion
5
+ export { insertedRow };
6
+
7
+ //# sourceMappingURL=inserted-row.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inserted-row.js","names":[],"sources":["../../src/repositories/inserted-row.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\n\nexport const insertedRow = sql<boolean>`(xmax = 0)`;\n"],"mappings":";;AAEA,MAAa,cAAc,GAAY"}
@@ -0,0 +1,17 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, MentionItemType } from "@headless-lms/core/types";
3
+ import { MentionsRepository } from "@headless-lms/core/discussion";
4
+ //#region src/repositories/mentions.d.ts
5
+ declare class DrizzleMentionsRepository implements MentionsRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]>;
10
+ forItems(orgId: string, itemType: MentionItemType, itemIds: string[]): Promise<Record<string, string[]>>;
11
+ sync(orgId: string, itemType: MentionItemType, itemId: string, orgUserIds: string[], at: Date): Promise<string[]>;
12
+ removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void>;
13
+ private item;
14
+ }
15
+ //#endregion
16
+ export { DrizzleMentionsRepository };
17
+ //# sourceMappingURL=mentions.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { mentions } from "../schema/discussion.js";
2
+ import "../schema/index.js";
3
+ import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
4
+ import { noopLogger } from "@headless-lms/core/shared/logger";
5
+ //#region src/repositories/mentions.ts
6
+ var DrizzleMentionsRepository = class {
7
+ db;
8
+ logger;
9
+ constructor(db, logger = noopLogger) {
10
+ this.db = db;
11
+ this.logger = logger;
12
+ }
13
+ async ofItem(orgId, itemType, itemId) {
14
+ return (await this.db.select({ orgUserId: mentions.orgUserId }).from(mentions).where(this.item(orgId, itemType, itemId)).orderBy(asc(mentions.createdAt), asc(mentions.orgUserId))).map((row) => row.orgUserId);
15
+ }
16
+ async forItems(orgId, itemType, itemIds) {
17
+ if (itemIds.length === 0) return {};
18
+ const rows = await this.db.select({
19
+ itemId: mentions.itemId,
20
+ orgUserId: mentions.orgUserId
21
+ }).from(mentions).where(and(eq(mentions.orgId, orgId), eq(mentions.itemType, itemType), inArray(mentions.itemId, itemIds))).orderBy(asc(mentions.itemId), asc(mentions.createdAt), asc(mentions.orgUserId));
22
+ const byItem = {};
23
+ for (const row of rows) (byItem[row.itemId] ??= []).push(row.orgUserId);
24
+ return byItem;
25
+ }
26
+ async sync(orgId, itemType, itemId, orgUserIds, at) {
27
+ const item = this.item(orgId, itemType, itemId);
28
+ await this.db.delete(mentions).where(orgUserIds.length > 0 ? and(item, notInArray(mentions.orgUserId, orgUserIds)) : item);
29
+ if (orgUserIds.length === 0) return [];
30
+ const added = await this.db.insert(mentions).values(orgUserIds.map((orgUserId) => ({
31
+ orgId,
32
+ itemType,
33
+ itemId,
34
+ orgUserId,
35
+ createdAt: at
36
+ }))).onConflictDoNothing().returning({ orgUserId: mentions.orgUserId });
37
+ this.logger.debug("mentions synced", {
38
+ orgId,
39
+ itemType,
40
+ itemId,
41
+ added: added.length
42
+ });
43
+ return added.map((row) => row.orgUserId);
44
+ }
45
+ async removeItem(orgId, itemType, itemId) {
46
+ await this.db.delete(mentions).where(this.item(orgId, itemType, itemId));
47
+ }
48
+ item(orgId, itemType, itemId) {
49
+ return and(eq(mentions.orgId, orgId), eq(mentions.itemType, itemType), eq(mentions.itemId, itemId));
50
+ }
51
+ };
52
+ //#endregion
53
+ export { DrizzleMentionsRepository };
54
+
55
+ //# sourceMappingURL=mentions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mentions.js","names":[],"sources":["../../src/repositories/mentions.ts"],"sourcesContent":["import { and, asc, eq, inArray, notInArray } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { MentionsRepository } from \"@headless-lms/core/discussion\";\nimport type { Logger, MentionItemType } from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { mentions } from \"../schema/index.js\";\n\nexport class DrizzleMentionsRepository implements MentionsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]> {\n const rows = await this.db\n .select({ orgUserId: mentions.orgUserId })\n .from(mentions)\n .where(this.item(orgId, itemType, itemId))\n .orderBy(asc(mentions.createdAt), asc(mentions.orgUserId));\n return rows.map((row) => row.orgUserId);\n }\n\n async forItems(\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n ): Promise<Record<string, string[]>> {\n if (itemIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({ itemId: mentions.itemId, orgUserId: mentions.orgUserId })\n .from(mentions)\n .where(\n and(\n eq(mentions.orgId, orgId),\n eq(mentions.itemType, itemType),\n inArray(mentions.itemId, itemIds),\n ),\n )\n .orderBy(asc(mentions.itemId), asc(mentions.createdAt), asc(mentions.orgUserId));\n const byItem: Record<string, string[]> = {};\n for (const row of rows) {\n (byItem[row.itemId] ??= []).push(row.orgUserId);\n }\n return byItem;\n }\n\n async sync(\n orgId: string,\n itemType: MentionItemType,\n itemId: string,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]> {\n const item = this.item(orgId, itemType, itemId);\n await this.db\n .delete(mentions)\n .where(orgUserIds.length > 0 ? and(item, notInArray(mentions.orgUserId, orgUserIds)) : item);\n if (orgUserIds.length === 0) {\n return [];\n }\n const added = await this.db\n .insert(mentions)\n .values(\n orgUserIds.map((orgUserId) => ({ orgId, itemType, itemId, orgUserId, createdAt: at })),\n )\n .onConflictDoNothing()\n .returning({ orgUserId: mentions.orgUserId });\n this.logger.debug(\"mentions synced\", { orgId, itemType, itemId, added: added.length });\n return added.map((row) => row.orgUserId);\n }\n\n async removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void> {\n await this.db.delete(mentions).where(this.item(orgId, itemType, itemId));\n }\n\n private item(orgId: string, itemType: MentionItemType, itemId: string) {\n return and(\n eq(mentions.orgId, orgId),\n eq(mentions.itemType, itemType),\n eq(mentions.itemId, itemId),\n );\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,UAA2B,QAAmC;EAMxF,QAAO,MALY,KAAK,GACrB,OAAO,EAAE,WAAW,SAAS,UAAU,CAAC,CAAC,CACzC,KAAK,QAAQ,CAAC,CACd,MAAM,KAAK,KAAK,OAAO,UAAU,MAAM,CAAC,CAAC,CACzC,QAAQ,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,SAAS,CAAC,EAAA,CAC/C,KAAK,QAAQ,IAAI,SAAS;CACxC;CAEA,MAAM,SACJ,OACA,UACA,SACmC;EACnC,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GAAE,QAAQ,SAAS;GAAQ,WAAW,SAAS;EAAU,CAAC,CAAC,CAClE,KAAK,QAAQ,CAAC,CACd,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,UAAU,QAAQ,GAC9B,QAAQ,SAAS,QAAQ,OAAO,CAClC,CACF,CAAC,CACA,QAAQ,IAAI,SAAS,MAAM,GAAG,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,SAAS,CAAC;EACjF,MAAM,SAAmC,CAAC;EAC1C,KAAK,MAAM,OAAO,MAChB,CAAC,OAAO,IAAI,YAAY,CAAC,EAAA,CAAG,KAAK,IAAI,SAAS;EAEhD,OAAO;CACT;CAEA,MAAM,KACJ,OACA,UACA,QACA,YACA,IACmB;EACnB,MAAM,OAAO,KAAK,KAAK,OAAO,UAAU,MAAM;EAC9C,MAAM,KAAK,GACR,OAAO,QAAQ,CAAC,CAChB,MAAM,WAAW,SAAS,IAAI,IAAI,MAAM,WAAW,SAAS,WAAW,UAAU,CAAC,IAAI,IAAI;EAC7F,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAEV,MAAM,QAAQ,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,OACC,WAAW,KAAK,eAAe;GAAE;GAAO;GAAU;GAAQ;GAAW,WAAW;EAAG,EAAE,CACvF,CAAC,CACA,oBAAoB,CAAC,CACrB,UAAU,EAAE,WAAW,SAAS,UAAU,CAAC;EAC9C,KAAK,OAAO,MAAM,mBAAmB;GAAE;GAAO;GAAU;GAAQ,OAAO,MAAM;EAAO,CAAC;EACrF,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS;CACzC;CAEA,MAAM,WAAW,OAAe,UAA2B,QAA+B;EACxF,MAAM,KAAK,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,KAAK,KAAK,OAAO,UAAU,MAAM,CAAC;CACzE;CAEA,KAAa,OAAe,UAA2B,QAAgB;EACrE,OAAO,IACL,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,UAAU,QAAQ,GAC9B,GAAG,SAAS,QAAQ,MAAM,CAC5B;CACF;AACF"}
@@ -0,0 +1,19 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, StoredSetting } from "@headless-lms/core/types";
3
+ import { NotificationSettingsRepository, SettingKey } from "@headless-lms/core/identity";
4
+ //#region src/repositories/notification-settings.d.ts
5
+ declare class DrizzleNotificationSettingsRepository implements NotificationSettingsRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ forUsers(orgId: string, userIds: string[], communityId: string | null, categories: string[] | null): Promise<(StoredSetting & {
10
+ userId: string;
11
+ })[]>;
12
+ apply(orgId: string, userId: string, upserts: (SettingKey & {
13
+ enabled: boolean;
14
+ })[], deletes: SettingKey[], at: Date): Promise<void>;
15
+ switchedOn(orgId: string, communityId: string, category: string, spaceId: string | null): Promise<string[]>;
16
+ }
17
+ //#endregion
18
+ export { DrizzleNotificationSettingsRepository };
19
+ //# sourceMappingURL=notification-settings.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { notificationSettings } from "../schema/identity.js";
2
+ import { orgUsers } from "../schema/organizations.js";
3
+ import "../schema/index.js";
4
+ import { translateDbErrors } from "./pg-errors.js";
5
+ import { and, eq, inArray, sql } from "drizzle-orm";
6
+ import { genId } from "@headless-lms/core/shared/id";
7
+ import { noopLogger } from "@headless-lms/core/shared/logger";
8
+ //#region src/repositories/notification-settings.ts
9
+ const ns = notificationSettings;
10
+ const communityKey = sql`coalesce(${ns.communityId}, '')`;
11
+ const spaceKey = sql`coalesce(${ns.spaceId}, '')`;
12
+ function matchesKey(key) {
13
+ return sql`(${communityKey} = ${key.communityId ?? ""} and ${spaceKey} = ${key.spaceId ?? ""} and ${ns.category} = ${key.category} and ${ns.channel} = ${key.channel})`;
14
+ }
15
+ var DrizzleNotificationSettingsRepository = class {
16
+ db;
17
+ logger;
18
+ constructor(db, logger = noopLogger) {
19
+ this.db = db;
20
+ this.logger = logger;
21
+ }
22
+ async forUsers(orgId, userIds, communityId, categories) {
23
+ if (userIds.length === 0) return [];
24
+ const conditions = [
25
+ eq(ns.orgId, orgId),
26
+ inArray(ns.userId, userIds),
27
+ sql`${communityKey} = ${communityId ?? ""}`
28
+ ];
29
+ if (categories !== null) {
30
+ if (categories.length === 0) return [];
31
+ conditions.push(inArray(ns.category, categories));
32
+ }
33
+ return this.db.select({
34
+ userId: ns.userId,
35
+ communityId: ns.communityId,
36
+ spaceId: ns.spaceId,
37
+ category: ns.category,
38
+ channel: ns.channel,
39
+ enabled: ns.enabled
40
+ }).from(ns).where(and(...conditions));
41
+ }
42
+ async apply(orgId, userId, upserts, deletes, at) {
43
+ if (upserts.length === 0 && deletes.length === 0) return;
44
+ await this.db.transaction(async (tx) => {
45
+ if (deletes.length > 0) await tx.delete(ns).where(and(eq(ns.orgId, orgId), eq(ns.userId, userId), sql`(${sql.join(deletes.map(matchesKey), sql` or `)})`));
46
+ if (upserts.length > 0) {
47
+ const rows = upserts.map((row) => sql`(${orgId}, ${genId("notificationSetting")}, ${userId}, ${row.communityId}, ${row.spaceId}, ${row.category}, ${row.channel}, ${row.enabled}, ${at.toISOString()}::timestamptz)`);
48
+ await tx.execute(sql`insert into notification_settings (org_id, id, user_id, community_id, space_id, category, channel, enabled, updated_at) values ${sql.join(rows, sql`, `)} on conflict (org_id, user_id, coalesce(community_id, ''), coalesce(space_id, ''), category, channel) do update set enabled = excluded.enabled, updated_at = excluded.updated_at`);
49
+ }
50
+ });
51
+ this.logger.debug("notification settings applied", {
52
+ orgId,
53
+ userId,
54
+ upserts: upserts.length,
55
+ deletes: deletes.length
56
+ });
57
+ }
58
+ async switchedOn(orgId, communityId, category, spaceId) {
59
+ const levels = spaceId === null ? [""] : [spaceId, ""];
60
+ return (await this.db.selectDistinct({ orgUserId: orgUsers.id }).from(ns).innerJoin(orgUsers, and(eq(orgUsers.orgId, ns.orgId), eq(orgUsers.userId, ns.userId))).where(and(eq(ns.orgId, orgId), sql`${communityKey} = ${communityId}`, eq(ns.category, category), sql`${spaceKey} in (${sql.join(levels.map((level) => sql`${level}`), sql`, `)})`, sql`${ns.enabled}`))).map((row) => row.orgUserId);
61
+ }
62
+ };
63
+ translateDbErrors(DrizzleNotificationSettingsRepository);
64
+ //#endregion
65
+ export { DrizzleNotificationSettingsRepository };
66
+
67
+ //# sourceMappingURL=notification-settings.js.map