@headless-lms/adapter-db 0.3.0 → 0.5.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/client.d.ts +8 -2
- package/dist/client.js +10 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +9 -3
- package/dist/repositories/admin.d.ts +12 -4
- package/dist/repositories/admin.js +165 -23
- package/dist/repositories/admin.js.map +1 -1
- package/dist/repositories/assets.d.ts +2 -2
- package/dist/repositories/assets.js +11 -5
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +0 -1
- package/dist/repositories/automations.js +2 -18
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +14 -5
- package/dist/repositories/chats.js +194 -85
- package/dist/repositories/chats.js.map +1 -1
- package/dist/repositories/content.d.ts +24 -7
- package/dist/repositories/content.js +229 -48
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/credentials.js +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 +7 -2
- package/dist/repositories/discussion.js +18 -7
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +11 -9
- package/dist/repositories/entitlements.js +56 -59
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/event-reports.d.ts +16 -0
- package/dist/repositories/event-reports.js +199 -0
- package/dist/repositories/event-reports.js.map +1 -0
- package/dist/repositories/grant-covers.d.ts +4 -3
- package/dist/repositories/grant-covers.js +7 -4
- package/dist/repositories/grant-covers.js.map +1 -1
- package/dist/repositories/identity.js +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/integrations.js +1 -1
- package/dist/repositories/member.d.ts +12 -4
- package/dist/repositories/member.js +19 -7
- package/dist/repositories/member.js.map +1 -1
- package/dist/repositories/members.d.ts +2 -5
- package/dist/repositories/members.js +39 -94
- 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 +23 -16
- package/dist/repositories/notifications.js +152 -44
- package/dist/repositories/notifications.js.map +1 -1
- package/dist/repositories/org-user-profile.d.ts +0 -17
- package/dist/repositories/org-user-profile.js +0 -1
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +4 -4
- package/dist/repositories/organizations.js +25 -47
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/outbox.d.ts +5 -1
- package/dist/repositories/outbox.js +12 -3
- package/dist/repositories/outbox.js.map +1 -1
- package/dist/repositories/posts.d.ts +4 -8
- package/dist/repositories/posts.js +26 -29
- package/dist/repositories/posts.js.map +1 -1
- package/dist/repositories/progress.js +1 -1
- package/dist/repositories/roles.d.ts +35 -0
- package/dist/repositories/roles.js +244 -0
- package/dist/repositories/roles.js.map +1 -0
- package/dist/repositories/settings.js +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/assets.d.ts +17 -0
- package/dist/schema/assets.js +1 -0
- package/dist/schema/assets.js.map +1 -1
- 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/content.d.ts +437 -3
- package/dist/schema/content.js +70 -3
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/discussion.d.ts +295 -128
- package/dist/schema/discussion.js +63 -26
- 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 +7 -6
- package/dist/schema/index.js +16 -7
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/organizations.d.ts +5 -22
- package/dist/schema/organizations.js +1 -13
- package/dist/schema/organizations.js.map +1 -1
- package/dist/schema/roles.d.ts +369 -0
- package/dist/schema/roles.js +71 -0
- package/dist/schema/roles.js.map +1 -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/0017_configurable_roles.sql +38 -0
- package/drizzle/0018_messages_client_id.sql +2 -0
- package/drizzle/0019_events.sql +44 -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/0017_snapshot.json +9518 -0
- package/drizzle/meta/0018_snapshot.json +9557 -0
- package/drizzle/meta/0019_snapshot.json +9919 -0
- package/drizzle/meta/_journal.json +63 -0
- package/package.json +2 -2
|
@@ -8,11 +8,16 @@ declare class DrizzleDiscussionRepository implements DiscussionRepository {
|
|
|
8
8
|
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
9
|
insertComment(orgId: string, comment: Comment): Promise<Comment>;
|
|
10
10
|
findComment(orgId: string, id: string): Promise<Comment | null>;
|
|
11
|
+
commenterIds(orgId: string, target: {
|
|
12
|
+
postId: string;
|
|
13
|
+
} | {
|
|
14
|
+
activityId: string;
|
|
15
|
+
}): Promise<string[]>;
|
|
11
16
|
updateComment(orgId: string, id: string, patch: Partial<Pick<Comment, "body" | "status" | "removedBy" | "updatedAt">>): Promise<Comment | null>;
|
|
12
17
|
listByActivity(orgId: string, activityId: string): Promise<Comment[]>;
|
|
13
18
|
listThreadsByPost(orgId: string, postId: string, query: PostThreadsQuery): Promise<Page<Comment>>;
|
|
14
19
|
reactionsOf(orgId: string, commentIds: string[], viewerId: string): Promise<Record<string, CommentReactions>>;
|
|
15
|
-
setReaction(orgId: string, commentId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<
|
|
20
|
+
setReaction(orgId: string, commentId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
|
|
16
21
|
authorsOf(orgId: string, orgUserIds: string[]): Promise<Record<string, AuthorRecord>>;
|
|
17
22
|
insertReport(orgId: string, report: CommentReport): Promise<CommentReport | null>;
|
|
18
23
|
listOpenFlagsForSpace(orgId: string, spaceId: string, limit: number): Promise<FlaggedComment[]>;
|
|
@@ -31,7 +36,7 @@ declare class DrizzleDiscussionRepository implements DiscussionRepository {
|
|
|
31
36
|
/** The same join and filters, counted. Separate from `listRows` because the
|
|
32
37
|
* total must ignore limit/offset. */
|
|
33
38
|
private listTotal;
|
|
34
|
-
listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentWithContext>>;
|
|
39
|
+
listComments(orgId: string, query: ListCommentsQuery, courseIds?: string[]): Promise<Page<CommentWithContext>>;
|
|
35
40
|
}
|
|
36
41
|
//#endregion
|
|
37
42
|
export { DrizzleDiscussionRepository };
|
|
@@ -1,13 +1,14 @@
|
|
|
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 {
|
|
9
|
+
import { insertedRow } from "./inserted-row.js";
|
|
10
10
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
11
|
+
import { and, asc, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm";
|
|
11
12
|
//#region src/repositories/discussion.ts
|
|
12
13
|
/** The title lives in the activity's opaque settings blob; the moderation card
|
|
13
14
|
* needs it to say "Lesson 3" rather than an id. */
|
|
@@ -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 {};
|
|
@@ -139,7 +144,6 @@ var DrizzleDiscussionRepository = class {
|
|
|
139
144
|
lastName: row.lastName,
|
|
140
145
|
email: row.email,
|
|
141
146
|
image: row.image ?? null,
|
|
142
|
-
role: row.role,
|
|
143
147
|
displayName: row.displayName ?? null,
|
|
144
148
|
headline: row.headline ?? null,
|
|
145
149
|
imageAssetOrgId: row.imageAssetOrgId ?? null,
|
|
@@ -213,8 +217,15 @@ var DrizzleDiscussionRepository = class {
|
|
|
213
217
|
listTotal(where) {
|
|
214
218
|
return this.db.select({ total: sql`cast(count(*) as int)` }).from(comments).innerJoin(activities, and(eq(activities.orgId, comments.orgId), eq(activities.id, comments.activityId))).where(where);
|
|
215
219
|
}
|
|
216
|
-
async listComments(orgId, query) {
|
|
220
|
+
async listComments(orgId, query, courseIds) {
|
|
221
|
+
if (courseIds !== void 0 && courseIds.length === 0) return {
|
|
222
|
+
rows: [],
|
|
223
|
+
total: 0,
|
|
224
|
+
page: query.page,
|
|
225
|
+
pageSize: query.pageSize
|
|
226
|
+
};
|
|
217
227
|
const conditions = [eq(comments.orgId, orgId)];
|
|
228
|
+
if (courseIds !== void 0) conditions.push(inArray(activities.courseId, courseIds));
|
|
218
229
|
if (query.status) conditions.push(eq(comments.status, query.status));
|
|
219
230
|
if (query.courseId) conditions.push(eq(activities.courseId, query.courseId));
|
|
220
231
|
if (query.activityId) conditions.push(eq(comments.activityId, query.activityId));
|
|
@@ -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} 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 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(\n orgId: string,\n query: ListCommentsQuery,\n courseIds?: string[],\n ): Promise<Page<CommentWithContext>> {\n if (courseIds !== undefined && courseIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const conditions: SQL[] = [eq(comments.orgId, orgId)];\n if (courseIds !== undefined) {\n conditions.push(inArray(activities.courseId, courseIds));\n }\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 { ContentType, Entitlement, EntitlementsQuery,
|
|
3
|
-
import { AccessFacts, EndingGrant, EndingGrantKey, EntitlementNotice, EntitlementsRepository, GrantKey, GrantTarget, InsertSpaceMemberInput, SpaceMembersQuery } from "@headless-lms/core/entitlements";
|
|
2
|
+
import { ContentType, Entitlement, EntitlementsQuery, Logger, Page, SpaceMember, SpaceMemberStatus } from "@headless-lms/core/types";
|
|
3
|
+
import { AccessFacts, CommunityReaders, EndingGrant, EndingGrantKey, EntitlementNotice, EntitlementsRepository, GrantKey, GrantTarget, InsertGrantInput, 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;
|
|
@@ -14,14 +14,14 @@ declare class DrizzleEntitlementsRepository implements EntitlementsRepository {
|
|
|
14
14
|
/** A grant targets exactly one of a bundle or a content item (db check
|
|
15
15
|
* constraint), so the upsert is inferred on whichever of the two partial
|
|
16
16
|
* uniques the target uses. */
|
|
17
|
-
insert(orgId: string, input:
|
|
18
|
-
insertIfAbsent(orgId: string, input: GrantEntitlementInput): Promise<Entitlement | null>;
|
|
17
|
+
insert(orgId: string, input: InsertGrantInput): Promise<Entitlement>;
|
|
19
18
|
lockGrant(orgId: string, key: GrantKey): Promise<Entitlement | null>;
|
|
20
|
-
setExpiresAt(orgId: string, id: string, expiresAt: Date | null): Promise<Entitlement | null>;
|
|
21
19
|
setStatus(orgId: string, id: string, status: "active" | "revoked"): Promise<Entitlement | null>;
|
|
22
20
|
hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;
|
|
23
21
|
listAccessibleContent(orgId: string, orgUserId: string, type?: ContentType): Promise<string[]>;
|
|
24
22
|
hasCommunityAccess(orgId: string, orgUserId: string, communityId: string): Promise<boolean>;
|
|
23
|
+
holdsAnyGrant(orgId: string, orgUserId: string): Promise<boolean>;
|
|
24
|
+
contentTypeOf(orgId: string, contentId: string): Promise<ContentType | null>;
|
|
25
25
|
private holds;
|
|
26
26
|
communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]>;
|
|
27
27
|
accessAmong(orgId: string, communityId: string, spaceId: string | null, orgUserIds: string[]): Promise<AccessFacts[]>;
|
|
@@ -32,17 +32,19 @@ declare class DrizzleEntitlementsRepository implements EntitlementsRepository {
|
|
|
32
32
|
insertSpaceMember(orgId: string, input: InsertSpaceMemberInput): Promise<SpaceMember>;
|
|
33
33
|
updateSpaceMemberStatus(orgId: string, id: string, status: SpaceMemberStatus): Promise<SpaceMember | null>;
|
|
34
34
|
deleteSpaceMember(orgId: string, id: string): Promise<void>;
|
|
35
|
-
countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number>;
|
|
35
|
+
countActiveSpaceMembers(orgId: string, spaceId: string, excluding: string[]): Promise<number>;
|
|
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
|
-
|
|
44
|
-
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
43
|
+
communityMemberIds(orgId: string, communityId: string, readerIds: string[]): Promise<string[]>;
|
|
45
44
|
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
45
|
+
lockCapacity(orgId: string, contentId: string): Promise<void>;
|
|
46
|
+
grantedOrgUserIds(orgId: string, contentId: string): Promise<string[]>;
|
|
47
|
+
openCommunityIds(orgId: string, orgUserId: string, readers: CommunityReaders): Promise<string[]>;
|
|
46
48
|
}
|
|
47
49
|
//#endregion
|
|
48
50
|
export { DrizzleEntitlementsRepository };
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { bundles, communities, contentItems, courses, downloads } from "../schema/content.js";
|
|
1
|
+
import { bundles, communities, contentItems, courses, downloads, events } from "../schema/content.js";
|
|
2
2
|
import { users } from "../schema/identity.js";
|
|
3
3
|
import { orgUsers } from "../schema/organizations.js";
|
|
4
4
|
import { contentAccess, entitlementEndingSoonNotices, entitlementExpiryNotices, entitlements, spaceMembers } from "../schema/entitlements.js";
|
|
5
5
|
import "../schema/index.js";
|
|
6
|
-
import { translateDbErrors } from "./pg-errors.js";
|
|
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";
|
|
6
|
+
import { pgErrorFields, translateDbErrors } from "./pg-errors.js";
|
|
7
|
+
import { communityMember, grantExpired, grantIsActive, grantOpens, holdsCommunity, oneOf } from "./grant-covers.js";
|
|
9
8
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
10
|
-
import {
|
|
9
|
+
import { and, asc, desc, eq, gt, ilike, inArray, or, sql } from "drizzle-orm";
|
|
10
|
+
import { NotFoundError } from "@headless-lms/core/shared/errors";
|
|
11
11
|
//#region src/repositories/entitlements.ts
|
|
12
|
-
const MODERATING_ROLES = ROLES.filter((role) => canModerate(role));
|
|
13
12
|
const derivedStatus = sql`case
|
|
14
13
|
when ${entitlements.status} = 'revoked' then 'revoked'
|
|
15
14
|
when ${grantExpired(entitlements.expiresAt)} then 'expired'
|
|
@@ -32,7 +31,7 @@ const endingSoonNoticed = sql`exists (
|
|
|
32
31
|
and ${entitlementEndingSoonNotices.expiresAt} = ${entitlements.expiresAt}
|
|
33
32
|
)`;
|
|
34
33
|
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})`;
|
|
34
|
+
const contentTitle = sql`coalesce(${courses.title}, ${downloads.title}, ${communities.title}, ${events.title}, ${bundles.name})`;
|
|
36
35
|
const sortColumns = {
|
|
37
36
|
firstName: users.firstName,
|
|
38
37
|
lastName: users.lastName,
|
|
@@ -82,7 +81,7 @@ var DrizzleEntitlementsRepository = class {
|
|
|
82
81
|
* LEFT JOIN per concrete content table (title), or bundles (name). Both target
|
|
83
82
|
* joins are LEFT — a row carries one of the two, never both. */
|
|
84
83
|
joined(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);
|
|
84
|
+
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))).leftJoin(events, and(eq(events.orgId, entitlements.orgId), eq(events.id, entitlements.contentId))).where(where);
|
|
86
85
|
}
|
|
87
86
|
async list(orgId, query) {
|
|
88
87
|
const conditions = [eq(entitlements.orgId, orgId)];
|
|
@@ -106,7 +105,7 @@ var DrizzleEntitlementsRepository = class {
|
|
|
106
105
|
} else orderBy = desc(entitlements.grantedAt);
|
|
107
106
|
const offset = (query.page - 1) * query.pageSize;
|
|
108
107
|
const rows = await this.joined(where).orderBy(orderBy).limit(query.pageSize).offset(offset);
|
|
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);
|
|
108
|
+
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))).leftJoin(events, and(eq(events.orgId, entitlements.orgId), eq(events.id, entitlements.contentId))).where(where);
|
|
110
109
|
return {
|
|
111
110
|
rows: rows.map(toEntitlement),
|
|
112
111
|
total,
|
|
@@ -129,61 +128,33 @@ var DrizzleEntitlementsRepository = class {
|
|
|
129
128
|
entitlements.orgUserId,
|
|
130
129
|
entitlements.contentId
|
|
131
130
|
];
|
|
132
|
-
const [row] = await this.db.insert(entitlements).values({
|
|
131
|
+
const [row] = await orgUserMustExist(input.orgUserId, this.db.insert(entitlements).values({
|
|
133
132
|
orgId,
|
|
134
133
|
orgUserId: input.orgUserId,
|
|
135
134
|
bundleId,
|
|
136
135
|
contentId,
|
|
137
136
|
status: "active",
|
|
138
|
-
source:
|
|
137
|
+
source: input.source,
|
|
139
138
|
grantedAt: /* @__PURE__ */ new Date(),
|
|
140
139
|
expiresAt: input.expiresAt
|
|
141
140
|
}).onConflictDoUpdate({
|
|
142
141
|
target,
|
|
143
142
|
set: {
|
|
144
143
|
status: "active",
|
|
145
|
-
source:
|
|
144
|
+
source: input.source,
|
|
146
145
|
grantedAt: /* @__PURE__ */ new Date(),
|
|
147
146
|
expiresAt: input.expiresAt
|
|
148
147
|
}
|
|
149
|
-
}).returning(selection);
|
|
148
|
+
}).returning(selection));
|
|
150
149
|
if (!row) throw new Error("failed to insert entitlement");
|
|
151
150
|
return toEntitlement(row);
|
|
152
151
|
}
|
|
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
152
|
async lockGrant(orgId, key) {
|
|
178
153
|
const target = key.bundleId ? eq(entitlements.bundleId, key.bundleId) : key.contentId ? eq(entitlements.contentId, key.contentId) : void 0;
|
|
179
154
|
if (!target) return null;
|
|
180
155
|
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
156
|
return row ? toEntitlement(row) : null;
|
|
182
157
|
}
|
|
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
|
-
}
|
|
187
158
|
async setStatus(orgId, id, status) {
|
|
188
159
|
const [row] = await this.db.update(entitlements).set({ status }).where(and(eq(entitlements.orgId, orgId), eq(entitlements.id, id))).returning(selection);
|
|
189
160
|
if (!row) return null;
|
|
@@ -198,6 +169,18 @@ var DrizzleEntitlementsRepository = class {
|
|
|
198
169
|
hasCommunityAccess(orgId, orgUserId, communityId) {
|
|
199
170
|
return this.holds(holdsCommunity(sql`${orgId}`, sql`${orgUserId}`, sql`${communityId}`));
|
|
200
171
|
}
|
|
172
|
+
holdsAnyGrant(orgId, orgUserId) {
|
|
173
|
+
return this.holds(sql`exists (
|
|
174
|
+
select 1 from ${entitlements}
|
|
175
|
+
where ${entitlements.orgId} = ${orgId}
|
|
176
|
+
and ${entitlements.orgUserId} = ${orgUserId}
|
|
177
|
+
and ${grantIsActive(entitlements)}
|
|
178
|
+
)`);
|
|
179
|
+
}
|
|
180
|
+
async contentTypeOf(orgId, contentId) {
|
|
181
|
+
const [row] = await this.db.select({ type: contentItems.type }).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
|
|
182
|
+
return row?.type ?? null;
|
|
183
|
+
}
|
|
201
184
|
async holds(predicate) {
|
|
202
185
|
const result = await this.db.execute(sql`select ${predicate} as covered`);
|
|
203
186
|
return (Array.isArray(result) ? result : result.rows)[0]?.covered === true;
|
|
@@ -222,13 +205,11 @@ var DrizzleEntitlementsRepository = class {
|
|
|
222
205
|
)` : sql`false`;
|
|
223
206
|
return (await this.db.select({
|
|
224
207
|
orgUserId: orgUsers.id,
|
|
225
|
-
|
|
226
|
-
member: sql`${communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`)}`,
|
|
208
|
+
granted: sql`${holdsCommunity(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${communityId}`)}`,
|
|
227
209
|
activeInSpace
|
|
228
210
|
}).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)))).map((row) => ({
|
|
229
211
|
orgUserId: row.orgUserId,
|
|
230
|
-
|
|
231
|
-
member: row.member === true,
|
|
212
|
+
granted: row.granted === true,
|
|
232
213
|
activeInSpace: row.activeInSpace === true
|
|
233
214
|
}));
|
|
234
215
|
}
|
|
@@ -244,7 +225,7 @@ var DrizzleEntitlementsRepository = class {
|
|
|
244
225
|
return row ? toSpaceMember(row) : null;
|
|
245
226
|
}
|
|
246
227
|
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);
|
|
228
|
+
const where = and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.spaceId, spaceId), query.statuses ? inArray(spaceMembers.status, query.statuses) : void 0, query.excluding?.length ? sql`not ${oneOf(sql`${spaceMembers.orgUserId}`, query.excluding)}` : void 0);
|
|
248
229
|
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
230
|
const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(spaceMembers).where(where);
|
|
250
231
|
return {
|
|
@@ -273,8 +254,8 @@ var DrizzleEntitlementsRepository = class {
|
|
|
273
254
|
async deleteSpaceMember(orgId, id) {
|
|
274
255
|
await this.db.delete(spaceMembers).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.id, id)));
|
|
275
256
|
}
|
|
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}`)));
|
|
257
|
+
async countActiveSpaceMembers(orgId, spaceId, excluding) {
|
|
258
|
+
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}`), excluding.length > 0 ? sql`not ${oneOf(sql`${spaceMembers.orgUserId}`, excluding)}` : void 0));
|
|
278
259
|
return row?.total ?? 0;
|
|
279
260
|
}
|
|
280
261
|
async activeSpaceIds(orgId, orgUserId, communityId) {
|
|
@@ -292,10 +273,10 @@ var DrizzleEntitlementsRepository = class {
|
|
|
292
273
|
communityId
|
|
293
274
|
}));
|
|
294
275
|
}
|
|
295
|
-
async
|
|
296
|
-
const expired = this.db.
|
|
297
|
-
const ending = this.db.
|
|
298
|
-
return
|
|
276
|
+
async orgIdsWithDueNotices(now, until) {
|
|
277
|
+
const expired = this.db.selectDistinct({ orgId: entitlements.orgId }).from(entitlements).where(this.expiredUnnoticed(null, now));
|
|
278
|
+
const ending = this.db.selectDistinct({ orgId: entitlements.orgId }).from(entitlements).innerJoin(communities, opensPublishedCommunity).where(this.endingUnnoticed(null, now, until));
|
|
279
|
+
return (await expired.union(ending)).map((row) => row.orgId).sort();
|
|
299
280
|
}
|
|
300
281
|
async claimNotice(orgId, notice, now) {
|
|
301
282
|
const statement = notice.kind === "expired" ? sql`insert into ${entitlementExpiryNotices} ("org_id", "entitlement_id", "expires_at", "notified_at")
|
|
@@ -309,21 +290,37 @@ var DrizzleEntitlementsRepository = class {
|
|
|
309
290
|
return (Array.isArray(result) ? result : result.rows).length > 0;
|
|
310
291
|
}
|
|
311
292
|
expiredUnnoticed(orgId, now) {
|
|
312
|
-
return and(eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt, at(now)), sql`not ${expiryNoticed}`);
|
|
293
|
+
return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt, at(now)), sql`not ${expiryNoticed}`);
|
|
313
294
|
}
|
|
314
295
|
endingUnnoticed(orgId, after, until) {
|
|
315
|
-
return and(eq(entitlements.orgId, orgId), grantIsActive(entitlements, at(after)), grantExpired(entitlements.expiresAt, at(until)), sql`not ${endingSoonNoticed}`);
|
|
296
|
+
return and(orgId === null ? void 0 : eq(entitlements.orgId, orgId), grantIsActive(entitlements, at(after)), grantExpired(entitlements.expiresAt, at(until)), sql`not ${endingSoonNoticed}`);
|
|
316
297
|
}
|
|
317
|
-
async
|
|
318
|
-
return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId),
|
|
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);
|
|
298
|
+
async communityMemberIds(orgId, communityId, readerIds) {
|
|
299
|
+
return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${communityId}`, readerIds))).orderBy(asc(orgUsers.id))).map((row) => row.id);
|
|
322
300
|
}
|
|
323
301
|
async communityIdsOpenedFor(orgId, orgUserId) {
|
|
324
302
|
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
303
|
}
|
|
304
|
+
async lockCapacity(orgId, contentId) {
|
|
305
|
+
await this.db.execute(sql`select pg_advisory_xact_lock(hashtext(${`entitlements:${orgId}:${contentId}`}))`);
|
|
306
|
+
}
|
|
307
|
+
async grantedOrgUserIds(orgId, contentId) {
|
|
308
|
+
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);
|
|
309
|
+
}
|
|
310
|
+
async openCommunityIds(orgId, orgUserId, readers) {
|
|
311
|
+
return (await this.db.select({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), readers.all ? void 0 : or(oneOf(sql`${communities.id}`, readers.ids), holdsCommunity(sql`${communities.orgId}`, sql`${orgUserId}`, sql`${communities.id}`)))).orderBy(asc(communities.id))).map((row) => row.id);
|
|
312
|
+
}
|
|
326
313
|
};
|
|
314
|
+
const ORG_USER_FK = "entitlements_org_id_org_user_id_org_users_org_id_id_fk";
|
|
315
|
+
async function orgUserMustExist(orgUserId, query) {
|
|
316
|
+
try {
|
|
317
|
+
return await query;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const pg = pgErrorFields(err);
|
|
320
|
+
if (pg?.code === "23503" && pg.constraint === ORG_USER_FK) throw new NotFoundError("Org user", orgUserId);
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
327
324
|
function toSpaceMember(row) {
|
|
328
325
|
return {
|
|
329
326
|
orgId: row.orgId,
|