@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.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/repositories/admin.js +1 -1
- package/dist/repositories/assets.d.ts +1 -2
- package/dist/repositories/assets.js +0 -4
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +0 -1
- package/dist/repositories/automations.js +1 -17
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +4 -4
- package/dist/repositories/chats.js +33 -18
- package/dist/repositories/chats.js.map +1 -1
- package/dist/repositories/content.d.ts +3 -3
- package/dist/repositories/content.js +35 -12
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/devices.d.ts +17 -0
- package/dist/repositories/devices.js +75 -0
- package/dist/repositories/devices.js.map +1 -0
- package/dist/repositories/digest-runs.d.ts +14 -0
- package/dist/repositories/digest-runs.js +39 -0
- package/dist/repositories/digest-runs.js.map +1 -0
- package/dist/repositories/discussion.d.ts +6 -1
- package/dist/repositories/discussion.js +9 -4
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +3 -1
- package/dist/repositories/entitlements.js +12 -6
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/inserted-row.d.ts +5 -0
- package/dist/repositories/inserted-row.js +7 -0
- package/dist/repositories/inserted-row.js.map +1 -0
- package/dist/repositories/mentions.d.ts +17 -0
- package/dist/repositories/mentions.js +55 -0
- package/dist/repositories/mentions.js.map +1 -0
- package/dist/repositories/notification-settings.d.ts +19 -0
- package/dist/repositories/notification-settings.js +67 -0
- package/dist/repositories/notification-settings.js.map +1 -0
- package/dist/repositories/notifications.d.ts +23 -16
- package/dist/repositories/notifications.js +152 -44
- package/dist/repositories/notifications.js.map +1 -1
- package/dist/repositories/organizations.d.ts +1 -0
- package/dist/repositories/organizations.js +4 -1
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/posts.d.ts +4 -8
- package/dist/repositories/posts.js +25 -28
- package/dist/repositories/posts.js.map +1 -1
- package/dist/repositories/subscriptions.d.ts +18 -0
- package/dist/repositories/subscriptions.js +72 -0
- package/dist/repositories/subscriptions.js.map +1 -0
- package/dist/schema/automations.d.ts +524 -19
- package/dist/schema/automations.js +73 -7
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/discussion.d.ts +277 -127
- package/dist/schema/discussion.js +59 -24
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/identity.d.ts +109 -109
- package/dist/schema/identity.js +28 -70
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +5 -5
- package/dist/schema/index.js +10 -6
- package/drizzle/0011_digest_runs.sql +8 -0
- package/drizzle/0012_subscriptions.sql +16 -0
- package/drizzle/0013_mentions.sql +10 -0
- package/drizzle/0014_notification_settings_devices.sql +36 -0
- package/drizzle/0015_automations_drop_source.sql +2 -0
- package/drizzle/0016_notifications_automations.sql +48 -0
- package/drizzle/meta/0011_snapshot.json +8868 -0
- package/drizzle/meta/0012_snapshot.json +8873 -0
- package/drizzle/meta/0013_snapshot.json +8943 -0
- package/drizzle/meta/0014_snapshot.json +9305 -0
- package/drizzle/meta/0015_snapshot.json +9270 -0
- package/drizzle/meta/0016_snapshot.json +9230 -0
- package/drizzle/meta/_journal.json +42 -0
- 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":"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"}
|
|
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
|
-
|
|
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
|
|
296
|
-
const expired = this.db.
|
|
297
|
-
const ending = this.db.
|
|
298
|
-
return
|
|
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 @@
|
|
|
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
|