@headless-lms/adapter-db 0.2.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 +12 -6
- package/dist/index.js +12 -6
- package/dist/repositories/admin.d.ts +23 -0
- package/dist/repositories/admin.js +393 -0
- package/dist/repositories/admin.js.map +1 -0
- package/dist/repositories/assets.d.ts +6 -3
- package/dist/repositories/assets.js +46 -9
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +5 -2
- package/dist/repositories/automations.js +29 -2
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +55 -0
- package/dist/repositories/chats.js +469 -0
- package/dist/repositories/chats.js.map +1 -0
- package/dist/repositories/content.d.ts +24 -1
- package/dist/repositories/content.js +316 -9
- 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 +16 -4
- package/dist/repositories/discussion.js +78 -18
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +31 -9
- package/dist/repositories/entitlements.js +213 -17
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/grant-covers.d.ts +20 -0
- package/dist/repositories/grant-covers.js +55 -0
- package/dist/repositories/grant-covers.js.map +1 -0
- package/dist/repositories/identity.d.ts +4 -2
- package/dist/repositories/identity.js +32 -1
- package/dist/repositories/identity.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/member.d.ts +16 -0
- package/dist/repositories/member.js +63 -0
- package/dist/repositories/member.js.map +1 -0
- package/dist/repositories/members.js +2 -2
- package/dist/repositories/members.js.map +1 -1
- 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 +30 -0
- package/dist/repositories/notifications.js +178 -0
- package/dist/repositories/notifications.js.map +1 -0
- package/dist/repositories/org-user-profile.d.ts +214 -1
- package/dist/repositories/org-user-profile.js +23 -2
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +4 -2
- package/dist/repositories/organizations.js +23 -8
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/pg-errors.js +3 -1
- package/dist/repositories/pg-errors.js.map +1 -1
- package/dist/repositories/posts.d.ts +35 -0
- package/dist/repositories/posts.js +252 -0
- package/dist/repositories/posts.js.map +1 -0
- 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/assets.d.ts +19 -2
- package/dist/schema/assets.js +10 -4
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +545 -2
- package/dist/schema/automations.js +73 -3
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/better-auth.d.ts +1613 -238
- package/dist/schema/better-auth.js +107 -22
- package/dist/schema/better-auth.js.map +1 -1
- package/dist/schema/content.d.ts +642 -5
- package/dist/schema/content.js +119 -10
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/discussion.d.ts +2182 -42
- package/dist/schema/discussion.js +461 -11
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/entitlements.d.ts +432 -1
- package/dist/schema/entitlements.js +109 -4
- package/dist/schema/entitlements.js.map +1 -1
- package/dist/schema/identity.d.ts +517 -1
- package/dist/schema/identity.js +76 -2
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +7 -7
- package/dist/schema/index.js +35 -8
- package/dist/schema/organizations.d.ts +4 -4
- package/dist/schema/organizations.js +2 -0
- package/dist/schema/organizations.js.map +1 -1
- package/drizzle/0003_community.sql +340 -0
- package/drizzle/0004_assets_sources.sql +1 -0
- package/drizzle/0005_notices_and_seed_index.sql +12 -0
- package/drizzle/0006_community_review_fixes.sql +24 -0
- package/drizzle/0007_notifications.sql +37 -0
- package/drizzle/0008_content_access.sql +13 -0
- package/drizzle/0009_community_indexes.sql +56 -0
- package/drizzle/0010_mcp_access.sql +114 -0
- 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/0003_snapshot.json +7061 -0
- package/drizzle/meta/0004_snapshot.json +7068 -0
- package/drizzle/meta/0005_snapshot.json +7180 -0
- package/drizzle/meta/0006_snapshot.json +7298 -0
- package/drizzle/meta/0007_snapshot.json +7620 -0
- package/drizzle/meta/0008_snapshot.json +7666 -0
- package/drizzle/meta/0009_snapshot.json +8185 -0
- package/drizzle/meta/0010_snapshot.json +8813 -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 +99 -1
- package/package.json +2 -2
- package/dist/repositories/course-analytics.d.ts +0 -15
- package/dist/repositories/course-analytics.js +0 -137
- package/dist/repositories/course-analytics.js.map +0 -1
- package/dist/repositories/dashboard.d.ts +0 -14
- package/dist/repositories/dashboard.js +0 -57
- package/dist/repositories/dashboard.js.map +0 -1
- package/dist/repositories/learn.d.ts +0 -19
- package/dist/repositories/learn.js +0 -56
- package/dist/repositories/learn.js.map +0 -1
- package/dist/repositories/students.d.ts +0 -16
- package/dist/repositories/students.js +0 -160
- package/dist/repositories/students.js.map +0 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { users } from "../schema/identity.js";
|
|
2
|
-
import { orgUsers } from "../schema/organizations.js";
|
|
3
1
|
import { activities } from "../schema/content.js";
|
|
4
|
-
import {
|
|
2
|
+
import { userProfiles, users } from "../schema/identity.js";
|
|
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
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { authorProfileColumns } from "./org-user-profile.js";
|
|
9
|
+
import { insertedRow } from "./inserted-row.js";
|
|
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
|
|
12
13
|
/** The title lives in the activity's opaque settings blob; the moderation card
|
|
@@ -25,6 +26,8 @@ function toComment(row) {
|
|
|
25
26
|
id: row.id,
|
|
26
27
|
orgId: row.orgId,
|
|
27
28
|
activityId: row.activityId,
|
|
29
|
+
postId: row.postId,
|
|
30
|
+
communityId: row.communityId,
|
|
28
31
|
parentId: row.parentId ?? null,
|
|
29
32
|
orgUserId: row.orgUserId,
|
|
30
33
|
body: row.body,
|
|
@@ -46,6 +49,8 @@ var DrizzleDiscussionRepository = class {
|
|
|
46
49
|
id: comment.id,
|
|
47
50
|
orgId,
|
|
48
51
|
activityId: comment.activityId,
|
|
52
|
+
postId: comment.postId,
|
|
53
|
+
communityId: comment.communityId,
|
|
49
54
|
parentId: comment.parentId,
|
|
50
55
|
orgUserId: comment.orgUserId,
|
|
51
56
|
body: comment.body,
|
|
@@ -60,6 +65,9 @@ var DrizzleDiscussionRepository = class {
|
|
|
60
65
|
const [row] = await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), eq(comments.id, id))).limit(1);
|
|
61
66
|
return row ? toComment(row) : null;
|
|
62
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
|
+
}
|
|
63
71
|
async updateComment(orgId, id, patch) {
|
|
64
72
|
const [row] = await this.db.update(comments).set({
|
|
65
73
|
..."body" in patch ? { body: patch.body } : {},
|
|
@@ -71,6 +79,25 @@ var DrizzleDiscussionRepository = class {
|
|
|
71
79
|
async listByActivity(orgId, activityId) {
|
|
72
80
|
return (await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), eq(comments.activityId, activityId))).orderBy(comments.createdAt)).map(toComment);
|
|
73
81
|
}
|
|
82
|
+
async listThreadsByPost(orgId, postId, query) {
|
|
83
|
+
const rootReadable = query.seesHeld ? void 0 : or(ne(comments.status, "pending"), eq(comments.orgUserId, query.viewerId));
|
|
84
|
+
const replyReadable = query.seesHeld ? sql`` : sql` and (rep.status = 'published' or rep.org_user_id = ${query.viewerId})`;
|
|
85
|
+
const where = and(eq(comments.orgId, orgId), eq(comments.postId, postId), isNull(comments.parentId), rootReadable, or(ne(comments.status, "removed"), sql`exists (
|
|
86
|
+
select 1 from comments rep
|
|
87
|
+
where rep.org_id = ${comments.orgId}
|
|
88
|
+
and rep.parent_id = ${comments.id}
|
|
89
|
+
and rep.status <> 'removed'${replyReadable}
|
|
90
|
+
)`));
|
|
91
|
+
const roots = await this.db.select().from(comments).where(where).orderBy(asc(comments.createdAt), asc(comments.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
92
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(comments).where(where);
|
|
93
|
+
const replies = roots.length === 0 ? [] : await this.db.select().from(comments).where(and(eq(comments.orgId, orgId), inArray(comments.parentId, roots.map((root) => root.id)))).orderBy(asc(comments.createdAt), asc(comments.id));
|
|
94
|
+
return {
|
|
95
|
+
rows: [...roots, ...replies].map(toComment),
|
|
96
|
+
total,
|
|
97
|
+
page: query.page,
|
|
98
|
+
pageSize: query.pageSize
|
|
99
|
+
};
|
|
100
|
+
}
|
|
74
101
|
async reactionsOf(orgId, commentIds, viewerId) {
|
|
75
102
|
if (commentIds.length === 0) return {};
|
|
76
103
|
const rows = await this.db.select({
|
|
@@ -90,9 +117,9 @@ var DrizzleDiscussionRepository = class {
|
|
|
90
117
|
async setReaction(orgId, commentId, orgUserId, emoji) {
|
|
91
118
|
if (emoji === null) {
|
|
92
119
|
await this.db.delete(commentReactions).where(and(eq(commentReactions.orgId, orgId), eq(commentReactions.commentId, commentId), eq(commentReactions.orgUserId, orgUserId)));
|
|
93
|
-
return;
|
|
120
|
+
return false;
|
|
94
121
|
}
|
|
95
|
-
await this.db.insert(commentReactions).values({
|
|
122
|
+
const [row] = await this.db.insert(commentReactions).values({
|
|
96
123
|
orgId,
|
|
97
124
|
commentId,
|
|
98
125
|
orgUserId,
|
|
@@ -104,14 +131,12 @@ var DrizzleDiscussionRepository = class {
|
|
|
104
131
|
commentReactions.orgUserId
|
|
105
132
|
],
|
|
106
133
|
set: { emoji }
|
|
107
|
-
});
|
|
134
|
+
}).returning({ inserted: insertedRow });
|
|
135
|
+
return row?.inserted === true;
|
|
108
136
|
}
|
|
109
137
|
async authorsOf(orgId, orgUserIds) {
|
|
110
138
|
if (orgUserIds.length === 0) return {};
|
|
111
|
-
const rows = await this.db.select(
|
|
112
|
-
...orgUserProfileColumns,
|
|
113
|
-
role: orgUsers.role
|
|
114
|
-
}).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
|
|
139
|
+
const rows = await this.db.select(authorProfileColumns).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
|
|
115
140
|
const byId = {};
|
|
116
141
|
for (const row of rows) byId[row.id] = {
|
|
117
142
|
id: row.id,
|
|
@@ -119,28 +144,63 @@ var DrizzleDiscussionRepository = class {
|
|
|
119
144
|
lastName: row.lastName,
|
|
120
145
|
email: row.email,
|
|
121
146
|
image: row.image ?? null,
|
|
122
|
-
role: row.role
|
|
147
|
+
role: row.role,
|
|
148
|
+
displayName: row.displayName ?? null,
|
|
149
|
+
headline: row.headline ?? null,
|
|
150
|
+
imageAssetOrgId: row.imageAssetOrgId ?? null,
|
|
151
|
+
imageAssetId: row.imageAssetId ?? null
|
|
123
152
|
};
|
|
124
153
|
return byId;
|
|
125
154
|
}
|
|
126
155
|
async insertReport(orgId, report) {
|
|
127
|
-
const [row] = await this.db.insert(
|
|
156
|
+
const [row] = await this.db.insert(commentFlags).values({
|
|
128
157
|
id: report.id,
|
|
129
158
|
orgId,
|
|
130
159
|
commentId: report.commentId,
|
|
131
160
|
orgUserId: report.orgUserId,
|
|
132
161
|
reason: report.reason,
|
|
133
162
|
resolvedAt: report.resolvedAt ? new Date(report.resolvedAt) : null,
|
|
163
|
+
resolution: report.resolution,
|
|
164
|
+
resolvedBy: report.resolvedBy,
|
|
134
165
|
createdAt: new Date(report.createdAt)
|
|
135
166
|
}).onConflictDoNothing().returning();
|
|
136
167
|
return row ?? null;
|
|
137
168
|
}
|
|
169
|
+
async listOpenFlagsForSpace(orgId, spaceId, limit) {
|
|
170
|
+
return (await this.db.select({
|
|
171
|
+
flag: commentFlags,
|
|
172
|
+
comment: {
|
|
173
|
+
id: comments.id,
|
|
174
|
+
postId: posts.id,
|
|
175
|
+
body: comments.body,
|
|
176
|
+
orgUserId: comments.orgUserId,
|
|
177
|
+
status: comments.status
|
|
178
|
+
}
|
|
179
|
+
}).from(commentFlags).innerJoin(comments, and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId))).innerJoin(posts, and(eq(posts.orgId, comments.orgId), eq(posts.id, comments.postId))).where(and(eq(commentFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(commentFlags.resolvedAt))).orderBy(asc(commentFlags.createdAt), asc(commentFlags.id)).limit(limit)).map((row) => ({
|
|
180
|
+
...row.flag,
|
|
181
|
+
comment: row.comment
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
async publishedCommentCounts(orgId, postIds) {
|
|
185
|
+
if (postIds.length === 0) return {};
|
|
186
|
+
const rows = await this.db.select({
|
|
187
|
+
postId: comments.postId,
|
|
188
|
+
count: sql`cast(count(*) as int)`
|
|
189
|
+
}).from(comments).where(and(eq(comments.orgId, orgId), inArray(comments.postId, postIds), eq(comments.status, "published"))).groupBy(comments.postId);
|
|
190
|
+
const byPost = {};
|
|
191
|
+
for (const row of rows) if (row.postId !== null) byPost[row.postId] = row.count;
|
|
192
|
+
return byPost;
|
|
193
|
+
}
|
|
138
194
|
async listOpenReports(orgId, commentIds) {
|
|
139
195
|
if (commentIds.length === 0) return [];
|
|
140
|
-
return await this.db.select().from(
|
|
196
|
+
return await this.db.select().from(commentFlags).where(and(eq(commentFlags.orgId, orgId), inArray(commentFlags.commentId, commentIds), isNull(commentFlags.resolvedAt))).orderBy(commentFlags.createdAt);
|
|
141
197
|
}
|
|
142
|
-
async resolveReportsFor(orgId, commentId,
|
|
143
|
-
await this.db.update(
|
|
198
|
+
async resolveReportsFor(orgId, commentId, resolved) {
|
|
199
|
+
await this.db.update(commentFlags).set({
|
|
200
|
+
resolvedAt: resolved.resolvedAt,
|
|
201
|
+
resolvedBy: resolved.resolvedBy,
|
|
202
|
+
resolution: resolved.resolution
|
|
203
|
+
}).where(and(eq(commentFlags.orgId, orgId), eq(commentFlags.commentId, commentId), isNull(commentFlags.resolvedAt)));
|
|
144
204
|
}
|
|
145
205
|
/** The list's one join: comments to their activity, which carries both the
|
|
146
206
|
* course (denormalised, so the module never enters the query) and the title.
|
|
@@ -166,7 +226,7 @@ var DrizzleDiscussionRepository = class {
|
|
|
166
226
|
if (query.orgUserId) conditions.push(eq(comments.orgUserId, query.orgUserId));
|
|
167
227
|
if (query.reported !== void 0) {
|
|
168
228
|
const hasOpenReport = sql`exists (
|
|
169
|
-
select 1 from ${
|
|
229
|
+
select 1 from ${commentFlags} r
|
|
170
230
|
where r.org_id = ${comments.orgId}
|
|
171
231
|
and r.comment_id = ${comments.id}
|
|
172
232
|
and r.resolved_at is null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discussion.js","names":[],"sources":["../../src/repositories/discussion.ts"],"sourcesContent":["import { and, asc, desc, eq, ilike, inArray, isNull, 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} from \"@headless-lms/core/discussion\";\nimport type {\n Comment,\n CommentReport,\n ListCommentsQuery,\n Logger,\n Page,\n ReactionEmoji,\n Role,\n} from \"@headless-lms/core/types\";\nimport { commentReactions, commentReports, comments } from \"../schema/index.js\";\nimport { activities } from \"../schema/content.js\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport { orgUserProfileColumns } 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 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 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 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({ ...orgUserProfileColumns, role: orgUsers.role })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\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 };\n }\n return byId;\n }\n\n async insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null> {\n const [row] = await this.db\n .insert(commentReports)\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 createdAt: new Date(report.createdAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ?? null;\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(commentReports)\n .where(\n and(\n eq(commentReports.orgId, orgId),\n inArray(commentReports.commentId, commentIds),\n isNull(commentReports.resolvedAt),\n ),\n )\n .orderBy(commentReports.createdAt);\n return rows;\n }\n\n async resolveReportsFor(orgId: string, commentId: string, resolvedAt: Date): Promise<void> {\n await this.db\n .update(commentReports)\n .set({ resolvedAt })\n .where(\n and(\n eq(commentReports.orgId, orgId),\n eq(commentReports.commentId, commentId),\n isNull(commentReports.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_reports: a comment with\n // three open reports must stay one row.\n const hasOpenReport = sql`exists (\n select 1 from ${commentReports} 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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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DbExecutor } from "../client.js";
|
|
2
|
-
import { Entitlement, EntitlementsQuery, GrantEntitlementInput, Logger, Page } from "@headless-lms/core/types";
|
|
3
|
-
import { EntitlementsRepository } from "@headless-lms/core/entitlements";
|
|
2
|
+
import { ContentType, Entitlement, EntitlementsQuery, GrantEntitlementInput, Logger, Page, SpaceMember, SpaceMemberStatus } from "@headless-lms/core/types";
|
|
3
|
+
import { AccessFacts, EndingGrant, EndingGrantKey, EntitlementNotice, EntitlementsRepository, GrantKey, GrantTarget, InsertSpaceMemberInput, SpaceMembersQuery } from "@headless-lms/core/entitlements";
|
|
4
4
|
//#region src/repositories/entitlements.d.ts
|
|
5
5
|
declare class DrizzleEntitlementsRepository implements EntitlementsRepository {
|
|
6
6
|
private readonly db;
|
|
@@ -15,14 +15,36 @@ declare class DrizzleEntitlementsRepository implements EntitlementsRepository {
|
|
|
15
15
|
* constraint), so the upsert is inferred on whichever of the two partial
|
|
16
16
|
* uniques the target uses. */
|
|
17
17
|
insert(orgId: string, input: GrantEntitlementInput): Promise<Entitlement>;
|
|
18
|
+
insertIfAbsent(orgId: string, input: GrantEntitlementInput): Promise<Entitlement | null>;
|
|
19
|
+
lockGrant(orgId: string, key: GrantKey): Promise<Entitlement | null>;
|
|
20
|
+
setExpiresAt(orgId: string, id: string, expiresAt: Date | null): Promise<Entitlement | null>;
|
|
18
21
|
setStatus(orgId: string, id: string, status: "active" | "revoked"): Promise<Entitlement | null>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;
|
|
23
|
+
listAccessibleContent(orgId: string, orgUserId: string, type?: ContentType): Promise<string[]>;
|
|
24
|
+
hasCommunityAccess(orgId: string, orgUserId: string, communityId: string): Promise<boolean>;
|
|
25
|
+
private holds;
|
|
26
|
+
communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]>;
|
|
27
|
+
accessAmong(orgId: string, communityId: string, spaceId: string | null, orgUserIds: string[]): Promise<AccessFacts[]>;
|
|
28
|
+
findSpaceMember(orgId: string, spaceId: string, orgUserId: string): Promise<SpaceMember | null>;
|
|
29
|
+
listSpaceMembersFor(orgId: string, orgUserId: string, communityId: string): Promise<SpaceMember[]>;
|
|
30
|
+
lockSpaceMember(orgId: string, spaceId: string, orgUserId: string): Promise<SpaceMember | null>;
|
|
31
|
+
listSpaceMembers(orgId: string, spaceId: string, query: SpaceMembersQuery): Promise<Page<SpaceMember>>;
|
|
32
|
+
insertSpaceMember(orgId: string, input: InsertSpaceMemberInput): Promise<SpaceMember>;
|
|
33
|
+
updateSpaceMemberStatus(orgId: string, id: string, status: SpaceMemberStatus): Promise<SpaceMember | null>;
|
|
34
|
+
deleteSpaceMember(orgId: string, id: string): Promise<void>;
|
|
35
|
+
countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number>;
|
|
36
|
+
activeSpaceIds(orgId: string, orgUserId: string, communityId: string): Promise<string[]>;
|
|
37
|
+
listGrantsExpiredBy(orgId: string, now: Date, limit: number, afterId?: string): Promise<Entitlement[]>;
|
|
38
|
+
listGrantsEndingBetween(orgId: string, after: Date, until: Date, limit: number, afterKey?: EndingGrantKey): Promise<EndingGrant[]>;
|
|
39
|
+
orgIdsWithDueNotices(now: Date, until: Date): Promise<string[]>;
|
|
40
|
+
claimNotice(orgId: string, notice: EntitlementNotice, now: Date): Promise<boolean>;
|
|
41
|
+
private expiredUnnoticed;
|
|
42
|
+
private endingUnnoticed;
|
|
43
|
+
communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
|
|
44
|
+
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
45
|
+
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
46
|
+
contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
|
|
47
|
+
openCommunityIds(orgId: string, orgUserId: string): Promise<string[]>;
|
|
26
48
|
}
|
|
27
49
|
//#endregion
|
|
28
50
|
export { DrizzleEntitlementsRepository };
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
+
import { bundles, communities, contentItems, courses, downloads } from "../schema/content.js";
|
|
1
2
|
import { users } from "../schema/identity.js";
|
|
2
3
|
import { orgUsers } from "../schema/organizations.js";
|
|
3
|
-
import {
|
|
4
|
-
import { entitlements } from "../schema/entitlements.js";
|
|
4
|
+
import { contentAccess, entitlementEndingSoonNotices, entitlementExpiryNotices, entitlements, spaceMembers } from "../schema/entitlements.js";
|
|
5
5
|
import "../schema/index.js";
|
|
6
6
|
import { translateDbErrors } from "./pg-errors.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { communityMember, grantExpired, grantIsActive, grantOpens, holdsCommunity } from "./grant-covers.js";
|
|
8
|
+
import { and, asc, desc, eq, gt, ilike, inArray, or, sql } from "drizzle-orm";
|
|
9
9
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
10
|
+
import { ROLES, canModerate } from "@headless-lms/core/organizations";
|
|
10
11
|
//#region src/repositories/entitlements.ts
|
|
12
|
+
const MODERATING_ROLES = ROLES.filter((role) => canModerate(role));
|
|
11
13
|
const derivedStatus = sql`case
|
|
12
14
|
when ${entitlements.status} = 'revoked' then 'revoked'
|
|
13
|
-
when ${entitlements.expiresAt}
|
|
15
|
+
when ${grantExpired(entitlements.expiresAt)} then 'expired'
|
|
14
16
|
else 'active'
|
|
15
17
|
end`;
|
|
16
|
-
|
|
18
|
+
function at(date) {
|
|
19
|
+
return sql`${date.toISOString()}::timestamptz`;
|
|
20
|
+
}
|
|
21
|
+
const expiryNoticed = sql`exists (
|
|
22
|
+
select 1 from ${entitlementExpiryNotices}
|
|
23
|
+
where ${entitlementExpiryNotices.orgId} = ${entitlements.orgId}
|
|
24
|
+
and ${entitlementExpiryNotices.entitlementId} = ${entitlements.id}
|
|
25
|
+
and ${entitlementExpiryNotices.expiresAt} = ${entitlements.expiresAt}
|
|
26
|
+
)`;
|
|
27
|
+
const endingSoonNoticed = sql`exists (
|
|
28
|
+
select 1 from ${entitlementEndingSoonNotices}
|
|
29
|
+
where ${entitlementEndingSoonNotices.orgId} = ${entitlements.orgId}
|
|
30
|
+
and ${entitlementEndingSoonNotices.entitlementId} = ${entitlements.id}
|
|
31
|
+
and ${entitlementEndingSoonNotices.communityId} = ${communities.id}
|
|
32
|
+
and ${entitlementEndingSoonNotices.expiresAt} = ${entitlements.expiresAt}
|
|
33
|
+
)`;
|
|
34
|
+
const opensPublishedCommunity = and(eq(communities.orgId, entitlements.orgId), eq(communities.status, "published"), grantOpens(entitlements, sql`${communities.id}`, "community"));
|
|
35
|
+
const contentTitle = sql`coalesce(${courses.title}, ${downloads.title}, ${communities.title}, ${bundles.name})`;
|
|
17
36
|
const sortColumns = {
|
|
18
37
|
firstName: users.firstName,
|
|
19
38
|
lastName: users.lastName,
|
|
@@ -63,7 +82,7 @@ var DrizzleEntitlementsRepository = class {
|
|
|
63
82
|
* LEFT JOIN per concrete content table (title), or bundles (name). Both target
|
|
64
83
|
* joins are LEFT — a row carries one of the two, never both. */
|
|
65
84
|
joined(where) {
|
|
66
|
-
return this.db.select(selection).from(entitlements).innerJoin(orgUsers, and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(contentItems, and(eq(contentItems.orgId, entitlements.orgId), eq(contentItems.id, entitlements.contentId))).leftJoin(bundles, and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId))).leftJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).leftJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).where(where);
|
|
85
|
+
return this.db.select(selection).from(entitlements).innerJoin(orgUsers, and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(contentItems, and(eq(contentItems.orgId, entitlements.orgId), eq(contentItems.id, entitlements.contentId))).leftJoin(bundles, and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId))).leftJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).leftJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).leftJoin(communities, and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId))).where(where);
|
|
67
86
|
}
|
|
68
87
|
async list(orgId, query) {
|
|
69
88
|
const conditions = [eq(entitlements.orgId, orgId)];
|
|
@@ -87,7 +106,7 @@ var DrizzleEntitlementsRepository = class {
|
|
|
87
106
|
} else orderBy = desc(entitlements.grantedAt);
|
|
88
107
|
const offset = (query.page - 1) * query.pageSize;
|
|
89
108
|
const rows = await this.joined(where).orderBy(orderBy).limit(query.pageSize).offset(offset);
|
|
90
|
-
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(entitlements).innerJoin(orgUsers, and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(contentItems, and(eq(contentItems.orgId, entitlements.orgId), eq(contentItems.id, entitlements.contentId))).leftJoin(bundles, and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId))).leftJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).leftJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).where(where);
|
|
109
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(entitlements).innerJoin(orgUsers, and(eq(orgUsers.orgId, entitlements.orgId), eq(orgUsers.id, entitlements.orgUserId))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(contentItems, and(eq(contentItems.orgId, entitlements.orgId), eq(contentItems.id, entitlements.contentId))).leftJoin(bundles, and(eq(bundles.orgId, entitlements.orgId), eq(bundles.id, entitlements.bundleId))).leftJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).leftJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).leftJoin(communities, and(eq(communities.orgId, entitlements.orgId), eq(communities.id, entitlements.contentId))).where(where);
|
|
91
110
|
return {
|
|
92
111
|
rows: rows.map(toEntitlement),
|
|
93
112
|
total,
|
|
@@ -131,22 +150,199 @@ var DrizzleEntitlementsRepository = class {
|
|
|
131
150
|
if (!row) throw new Error("failed to insert entitlement");
|
|
132
151
|
return toEntitlement(row);
|
|
133
152
|
}
|
|
153
|
+
async insertIfAbsent(orgId, input) {
|
|
154
|
+
const bundleId = input.bundleId ?? null;
|
|
155
|
+
const contentId = input.contentId ?? null;
|
|
156
|
+
const target = bundleId ? [
|
|
157
|
+
entitlements.orgId,
|
|
158
|
+
entitlements.orgUserId,
|
|
159
|
+
entitlements.bundleId
|
|
160
|
+
] : [
|
|
161
|
+
entitlements.orgId,
|
|
162
|
+
entitlements.orgUserId,
|
|
163
|
+
entitlements.contentId
|
|
164
|
+
];
|
|
165
|
+
const [row] = await this.db.insert(entitlements).values({
|
|
166
|
+
orgId,
|
|
167
|
+
orgUserId: input.orgUserId,
|
|
168
|
+
bundleId,
|
|
169
|
+
contentId,
|
|
170
|
+
status: "active",
|
|
171
|
+
source: "manual",
|
|
172
|
+
grantedAt: /* @__PURE__ */ new Date(),
|
|
173
|
+
expiresAt: input.expiresAt
|
|
174
|
+
}).onConflictDoNothing({ target }).returning(selection);
|
|
175
|
+
return row ? toEntitlement(row) : null;
|
|
176
|
+
}
|
|
177
|
+
async lockGrant(orgId, key) {
|
|
178
|
+
const target = key.bundleId ? eq(entitlements.bundleId, key.bundleId) : key.contentId ? eq(entitlements.contentId, key.contentId) : void 0;
|
|
179
|
+
if (!target) return null;
|
|
180
|
+
const [row] = await this.db.select(selection).from(entitlements).where(and(eq(entitlements.orgId, orgId), eq(entitlements.orgUserId, key.orgUserId), target)).for("update").limit(1);
|
|
181
|
+
return row ? toEntitlement(row) : null;
|
|
182
|
+
}
|
|
183
|
+
async setExpiresAt(orgId, id, expiresAt) {
|
|
184
|
+
const [row] = await this.db.update(entitlements).set({ expiresAt }).where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id))).returning(selection);
|
|
185
|
+
return row ? toEntitlement(row) : null;
|
|
186
|
+
}
|
|
134
187
|
async setStatus(orgId, id, status) {
|
|
135
188
|
const [row] = await this.db.update(entitlements).set({ status }).where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id))).returning(selection);
|
|
136
189
|
if (!row) return null;
|
|
137
190
|
return toEntitlement(row);
|
|
138
191
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
192
|
+
async hasAccess(orgId, orgUserId, contentId) {
|
|
193
|
+
return (await this.db.select({ id: contentAccess.entitlementId }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.orgUserId, orgUserId), eq(contentAccess.contentId, contentId))).limit(1)).length > 0;
|
|
194
|
+
}
|
|
195
|
+
async listAccessibleContent(orgId, orgUserId, type) {
|
|
196
|
+
return (await this.db.selectDistinct({ contentId: contentAccess.contentId }).from(contentAccess).innerJoin(contentItems, and(eq(contentItems.orgId, contentAccess.orgId), eq(contentItems.id, contentAccess.contentId))).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.orgUserId, orgUserId), type ? eq(contentItems.type, type) : void 0))).map((r) => r.contentId);
|
|
197
|
+
}
|
|
198
|
+
hasCommunityAccess(orgId, orgUserId, communityId) {
|
|
199
|
+
return this.holds(holdsCommunity(sql`${orgId}`, sql`${orgUserId}`, sql`${communityId}`));
|
|
200
|
+
}
|
|
201
|
+
async holds(predicate) {
|
|
202
|
+
const result = await this.db.execute(sql`select ${predicate} as covered`);
|
|
203
|
+
return (Array.isArray(result) ? result : result.rows)[0]?.covered === true;
|
|
204
|
+
}
|
|
205
|
+
async communitiesOpenedBy(orgId, grant) {
|
|
206
|
+
if (!grant.contentId && !grant.bundleId) return [];
|
|
207
|
+
const target = {
|
|
208
|
+
orgId: sql`${orgId}`,
|
|
209
|
+
contentId: sql`${grant.contentId ?? null}::text`,
|
|
210
|
+
bundleId: sql`${grant.bundleId ?? null}::text`
|
|
211
|
+
};
|
|
212
|
+
return (await this.db.select({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), eq(communities.status, "published"), grantOpens(target, sql`${communities.id}`, "community"))).orderBy(asc(communities.id))).map((row) => row.id);
|
|
213
|
+
}
|
|
214
|
+
async accessAmong(orgId, communityId, spaceId, orgUserIds) {
|
|
215
|
+
if (orgUserIds.length === 0) return [];
|
|
216
|
+
const activeInSpace = spaceId ? sql`exists (
|
|
217
|
+
select 1 from space_members sm
|
|
218
|
+
where sm.org_id = ${sql`${orgUsers.orgId}`}
|
|
219
|
+
and sm.space_id = ${spaceId}
|
|
220
|
+
and sm.org_user_id = ${sql`${orgUsers.id}`}
|
|
221
|
+
and sm.status = 'active'
|
|
222
|
+
)` : sql`false`;
|
|
223
|
+
return (await this.db.select({
|
|
224
|
+
orgUserId: orgUsers.id,
|
|
225
|
+
role: orgUsers.role,
|
|
226
|
+
member: sql`${communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`)}`,
|
|
227
|
+
activeInSpace
|
|
228
|
+
}).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)))).map((row) => ({
|
|
229
|
+
orgUserId: row.orgUserId,
|
|
230
|
+
role: row.role,
|
|
231
|
+
member: row.member === true,
|
|
232
|
+
activeInSpace: row.activeInSpace === true
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
async findSpaceMember(orgId, spaceId, orgUserId) {
|
|
236
|
+
const [row] = await this.db.select().from(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.spaceId, spaceId), eq(spaceMembers.orgUserId, orgUserId))).limit(1);
|
|
237
|
+
return row ? toSpaceMember(row) : null;
|
|
238
|
+
}
|
|
239
|
+
async listSpaceMembersFor(orgId, orgUserId, communityId) {
|
|
240
|
+
return (await this.db.select().from(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.orgUserId, orgUserId), eq(spaceMembers.communityId, communityId)))).map(toSpaceMember);
|
|
241
|
+
}
|
|
242
|
+
async lockSpaceMember(orgId, spaceId, orgUserId) {
|
|
243
|
+
const [row] = await this.db.select().from(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.spaceId, spaceId), eq(spaceMembers.orgUserId, orgUserId))).for("update").limit(1);
|
|
244
|
+
return row ? toSpaceMember(row) : null;
|
|
245
|
+
}
|
|
246
|
+
async listSpaceMembers(orgId, spaceId, query) {
|
|
247
|
+
const where = and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.spaceId, spaceId), query.statuses ? inArray(spaceMembers.status, query.statuses) : void 0);
|
|
248
|
+
const rows = await this.db.select().from(spaceMembers).where(where).orderBy(asc(spaceMembers.createdAt), asc(spaceMembers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
249
|
+
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(spaceMembers).where(where);
|
|
250
|
+
return {
|
|
251
|
+
rows: rows.map(toSpaceMember),
|
|
252
|
+
total,
|
|
253
|
+
page: query.page,
|
|
254
|
+
pageSize: query.pageSize
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async insertSpaceMember(orgId, input) {
|
|
258
|
+
const [row] = await this.db.insert(spaceMembers).values({
|
|
259
|
+
orgId,
|
|
260
|
+
communityId: input.communityId,
|
|
261
|
+
spaceId: input.spaceId,
|
|
262
|
+
orgUserId: input.orgUserId,
|
|
263
|
+
status: input.status,
|
|
264
|
+
source: input.source
|
|
265
|
+
}).returning();
|
|
266
|
+
if (!row) throw new Error("failed to insert space member");
|
|
267
|
+
return toSpaceMember(row);
|
|
268
|
+
}
|
|
269
|
+
async updateSpaceMemberStatus(orgId, id, status) {
|
|
270
|
+
const [row] = await this.db.update(spaceMembers).set({ status }).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id))).returning();
|
|
271
|
+
return row ? toSpaceMember(row) : null;
|
|
272
|
+
}
|
|
273
|
+
async deleteSpaceMember(orgId, id) {
|
|
274
|
+
await this.db.delete(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)));
|
|
275
|
+
}
|
|
276
|
+
async countActiveSpaceMembers(orgId, spaceId) {
|
|
277
|
+
const [row] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.spaceId, spaceId), eq(spaceMembers.status, "active"), holdsCommunity(sql`${spaceMembers.orgId}`, sql`${spaceMembers.orgUserId}`, sql`${spaceMembers.communityId}`)));
|
|
278
|
+
return row?.total ?? 0;
|
|
279
|
+
}
|
|
280
|
+
async activeSpaceIds(orgId, orgUserId, communityId) {
|
|
281
|
+
return (await this.db.select({ spaceId: spaceMembers.spaceId }).from(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.orgUserId, orgUserId), eq(spaceMembers.communityId, communityId), eq(spaceMembers.status, "active")))).map((row) => row.spaceId);
|
|
282
|
+
}
|
|
283
|
+
async listGrantsExpiredBy(orgId, now, limit, afterId) {
|
|
284
|
+
return (await this.db.select(selection).from(entitlements).where(and(this.expiredUnnoticed(orgId, now), afterId === void 0 ? void 0 : gt(entitlements.id, afterId))).orderBy(asc(entitlements.id)).limit(limit)).map(toEntitlement);
|
|
285
|
+
}
|
|
286
|
+
async listGrantsEndingBetween(orgId, after, until, limit, afterKey) {
|
|
287
|
+
return (await this.db.select({
|
|
288
|
+
...selection,
|
|
289
|
+
communityId: communities.id
|
|
290
|
+
}).from(entitlements).innerJoin(communities, opensPublishedCommunity).where(and(this.endingUnnoticed(orgId, after, until), afterKey ? sql`(${entitlements.id}, ${communities.id}) > (${afterKey.entitlementId}, ${afterKey.communityId})` : void 0)).orderBy(asc(entitlements.id), asc(communities.id)).limit(limit)).map(({ communityId, ...grant }) => ({
|
|
291
|
+
grant: toEntitlement(grant),
|
|
292
|
+
communityId
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
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
|
+
}
|
|
300
|
+
async claimNotice(orgId, notice, now) {
|
|
301
|
+
const statement = notice.kind === "expired" ? sql`insert into ${entitlementExpiryNotices} ("org_id", "entitlement_id", "expires_at", "notified_at")
|
|
302
|
+
values (${orgId}, ${notice.entitlementId}, ${at(notice.expiresAt)}, ${at(now)})
|
|
303
|
+
on conflict do nothing
|
|
304
|
+
returning "entitlement_id"` : sql`insert into ${entitlementEndingSoonNotices} ("org_id", "entitlement_id", "community_id", "expires_at", "notified_at")
|
|
305
|
+
values (${orgId}, ${notice.entitlementId}, ${notice.communityId}, ${at(notice.expiresAt)}, ${at(now)})
|
|
306
|
+
on conflict do nothing
|
|
307
|
+
returning "entitlement_id"`;
|
|
308
|
+
const result = await this.db.execute(statement);
|
|
309
|
+
return (Array.isArray(result) ? result : result.rows).length > 0;
|
|
310
|
+
}
|
|
311
|
+
expiredUnnoticed(orgId, now) {
|
|
312
|
+
return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt, at(now)), sql`not ${expiryNoticed}`);
|
|
313
|
+
}
|
|
314
|
+
endingUnnoticed(orgId, after, until) {
|
|
315
|
+
return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), grantIsActive(entitlements, at(after)), grantExpired(entitlements.expiresAt, at(until)), sql`not ${endingSoonNoticed}`);
|
|
316
|
+
}
|
|
317
|
+
async communityModeratorIds(orgId, communityId) {
|
|
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);
|
|
319
|
+
}
|
|
320
|
+
async communityMemberIds(orgId, communityId) {
|
|
321
|
+
return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`))).orderBy(asc(orgUsers.id))).map((row) => row.id);
|
|
322
|
+
}
|
|
323
|
+
async communityIdsOpenedFor(orgId, orgUserId) {
|
|
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
|
+
}
|
|
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);
|
|
148
331
|
}
|
|
149
332
|
};
|
|
333
|
+
function toSpaceMember(row) {
|
|
334
|
+
return {
|
|
335
|
+
orgId: row.orgId,
|
|
336
|
+
id: row.id,
|
|
337
|
+
communityId: row.communityId,
|
|
338
|
+
spaceId: row.spaceId,
|
|
339
|
+
orgUserId: row.orgUserId,
|
|
340
|
+
status: row.status,
|
|
341
|
+
source: row.source,
|
|
342
|
+
createdAt: row.createdAt,
|
|
343
|
+
updatedAt: row.updatedAt
|
|
344
|
+
};
|
|
345
|
+
}
|
|
150
346
|
translateDbErrors(DrizzleEntitlementsRepository);
|
|
151
347
|
//#endregion
|
|
152
348
|
export { DrizzleEntitlementsRepository };
|