@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.
Files changed (123) hide show
  1. package/dist/client.d.ts +8 -2
  2. package/dist/client.js +10 -2
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +10 -4
  5. package/dist/index.js +9 -3
  6. package/dist/repositories/admin.d.ts +12 -4
  7. package/dist/repositories/admin.js +165 -23
  8. package/dist/repositories/admin.js.map +1 -1
  9. package/dist/repositories/assets.d.ts +2 -2
  10. package/dist/repositories/assets.js +11 -5
  11. package/dist/repositories/assets.js.map +1 -1
  12. package/dist/repositories/automations.d.ts +0 -1
  13. package/dist/repositories/automations.js +2 -18
  14. package/dist/repositories/automations.js.map +1 -1
  15. package/dist/repositories/chats.d.ts +14 -5
  16. package/dist/repositories/chats.js +194 -85
  17. package/dist/repositories/chats.js.map +1 -1
  18. package/dist/repositories/content.d.ts +24 -7
  19. package/dist/repositories/content.js +229 -48
  20. package/dist/repositories/content.js.map +1 -1
  21. package/dist/repositories/credentials.js +1 -1
  22. package/dist/repositories/devices.d.ts +17 -0
  23. package/dist/repositories/devices.js +75 -0
  24. package/dist/repositories/devices.js.map +1 -0
  25. package/dist/repositories/digest-runs.d.ts +14 -0
  26. package/dist/repositories/digest-runs.js +39 -0
  27. package/dist/repositories/digest-runs.js.map +1 -0
  28. package/dist/repositories/discussion.d.ts +7 -2
  29. package/dist/repositories/discussion.js +18 -7
  30. package/dist/repositories/discussion.js.map +1 -1
  31. package/dist/repositories/entitlements.d.ts +11 -9
  32. package/dist/repositories/entitlements.js +56 -59
  33. package/dist/repositories/entitlements.js.map +1 -1
  34. package/dist/repositories/event-reports.d.ts +16 -0
  35. package/dist/repositories/event-reports.js +199 -0
  36. package/dist/repositories/event-reports.js.map +1 -0
  37. package/dist/repositories/grant-covers.d.ts +4 -3
  38. package/dist/repositories/grant-covers.js +7 -4
  39. package/dist/repositories/grant-covers.js.map +1 -1
  40. package/dist/repositories/identity.js +1 -1
  41. package/dist/repositories/inserted-row.d.ts +5 -0
  42. package/dist/repositories/inserted-row.js +7 -0
  43. package/dist/repositories/inserted-row.js.map +1 -0
  44. package/dist/repositories/integrations.js +1 -1
  45. package/dist/repositories/member.d.ts +12 -4
  46. package/dist/repositories/member.js +19 -7
  47. package/dist/repositories/member.js.map +1 -1
  48. package/dist/repositories/members.d.ts +2 -5
  49. package/dist/repositories/members.js +39 -94
  50. package/dist/repositories/members.js.map +1 -1
  51. package/dist/repositories/mentions.d.ts +17 -0
  52. package/dist/repositories/mentions.js +55 -0
  53. package/dist/repositories/mentions.js.map +1 -0
  54. package/dist/repositories/notification-settings.d.ts +19 -0
  55. package/dist/repositories/notification-settings.js +67 -0
  56. package/dist/repositories/notification-settings.js.map +1 -0
  57. package/dist/repositories/notifications.d.ts +23 -16
  58. package/dist/repositories/notifications.js +152 -44
  59. package/dist/repositories/notifications.js.map +1 -1
  60. package/dist/repositories/org-user-profile.d.ts +0 -17
  61. package/dist/repositories/org-user-profile.js +0 -1
  62. package/dist/repositories/org-user-profile.js.map +1 -1
  63. package/dist/repositories/organizations.d.ts +4 -4
  64. package/dist/repositories/organizations.js +25 -47
  65. package/dist/repositories/organizations.js.map +1 -1
  66. package/dist/repositories/outbox.d.ts +5 -1
  67. package/dist/repositories/outbox.js +12 -3
  68. package/dist/repositories/outbox.js.map +1 -1
  69. package/dist/repositories/posts.d.ts +4 -8
  70. package/dist/repositories/posts.js +26 -29
  71. package/dist/repositories/posts.js.map +1 -1
  72. package/dist/repositories/progress.js +1 -1
  73. package/dist/repositories/roles.d.ts +35 -0
  74. package/dist/repositories/roles.js +244 -0
  75. package/dist/repositories/roles.js.map +1 -0
  76. package/dist/repositories/settings.js +1 -1
  77. package/dist/repositories/subscriptions.d.ts +18 -0
  78. package/dist/repositories/subscriptions.js +72 -0
  79. package/dist/repositories/subscriptions.js.map +1 -0
  80. package/dist/schema/assets.d.ts +17 -0
  81. package/dist/schema/assets.js +1 -0
  82. package/dist/schema/assets.js.map +1 -1
  83. package/dist/schema/automations.d.ts +524 -19
  84. package/dist/schema/automations.js +73 -7
  85. package/dist/schema/automations.js.map +1 -1
  86. package/dist/schema/content.d.ts +437 -3
  87. package/dist/schema/content.js +70 -3
  88. package/dist/schema/content.js.map +1 -1
  89. package/dist/schema/discussion.d.ts +295 -128
  90. package/dist/schema/discussion.js +63 -26
  91. package/dist/schema/discussion.js.map +1 -1
  92. package/dist/schema/identity.d.ts +109 -109
  93. package/dist/schema/identity.js +28 -70
  94. package/dist/schema/identity.js.map +1 -1
  95. package/dist/schema/index.d.ts +7 -6
  96. package/dist/schema/index.js +16 -7
  97. package/dist/schema/index.js.map +1 -1
  98. package/dist/schema/organizations.d.ts +5 -22
  99. package/dist/schema/organizations.js +1 -13
  100. package/dist/schema/organizations.js.map +1 -1
  101. package/dist/schema/roles.d.ts +369 -0
  102. package/dist/schema/roles.js +71 -0
  103. package/dist/schema/roles.js.map +1 -0
  104. package/drizzle/0011_digest_runs.sql +8 -0
  105. package/drizzle/0012_subscriptions.sql +16 -0
  106. package/drizzle/0013_mentions.sql +10 -0
  107. package/drizzle/0014_notification_settings_devices.sql +36 -0
  108. package/drizzle/0015_automations_drop_source.sql +2 -0
  109. package/drizzle/0016_notifications_automations.sql +48 -0
  110. package/drizzle/0017_configurable_roles.sql +38 -0
  111. package/drizzle/0018_messages_client_id.sql +2 -0
  112. package/drizzle/0019_events.sql +44 -0
  113. package/drizzle/meta/0011_snapshot.json +8868 -0
  114. package/drizzle/meta/0012_snapshot.json +8873 -0
  115. package/drizzle/meta/0013_snapshot.json +8943 -0
  116. package/drizzle/meta/0014_snapshot.json +9305 -0
  117. package/drizzle/meta/0015_snapshot.json +9270 -0
  118. package/drizzle/meta/0016_snapshot.json +9230 -0
  119. package/drizzle/meta/0017_snapshot.json +9518 -0
  120. package/drizzle/meta/0018_snapshot.json +9557 -0
  121. package/drizzle/meta/0019_snapshot.json +9919 -0
  122. package/drizzle/meta/_journal.json +63 -0
  123. package/package.json +2 -2
@@ -4,15 +4,15 @@ import { orgUsers } from "../schema/organizations.js";
4
4
  import { contentAccess, spaceMembers } from "../schema/entitlements.js";
5
5
  import "../schema/index.js";
6
6
  import { translateDbErrors } from "./pg-errors.js";
7
- import { communityMember, communityPublished } from "./grant-covers.js";
8
- import { and, asc, eq, ilike, inArray, sql } from "drizzle-orm";
7
+ import { communityMember, communityPublished, oneOf } from "./grant-covers.js";
8
+ import { directEventGrantIds, eventAttendeePage, eventFacesFor, memberEventIds } from "./event-reports.js";
9
9
  import { noopLogger } from "@headless-lms/core/shared/logger";
10
+ import { and, asc, eq, ilike, inArray, sql } from "drizzle-orm";
10
11
  //#region src/repositories/member.ts
11
12
  function personColumns(orgId) {
12
13
  return {
13
14
  orgUserId: orgUsers.id,
14
15
  userId: orgUsers.userId,
15
- role: orgUsers.role,
16
16
  displayName: sql`coalesce(nullif(trim(${userProfiles.displayName}), ''), nullif(trim(concat_ws(' ', ${users.firstName}, ${users.lastName})), ''))`,
17
17
  headline: userProfiles.headline,
18
18
  bio: userProfiles.bio,
@@ -26,10 +26,10 @@ var DrizzleMemberRepository = class {
26
26
  this.db = db;
27
27
  this.logger = logger;
28
28
  }
29
- async directory(orgId, communityId, query) {
29
+ async directory(orgId, communityId, query, readerIds) {
30
30
  const columns = personColumns(orgId);
31
31
  const q = query.search?.trim();
32
- const where = and(eq(orgUsers.orgId, orgId), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`), q ? ilike(columns.displayName, `%${q}%`) : void 0);
32
+ const where = and(eq(orgUsers.orgId, orgId), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${communityId}`, readerIds), q ? ilike(columns.displayName, `%${q}%`) : void 0);
33
33
  const rows = await this.db.select(columns).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(where).orderBy(asc(userProfiles.displayName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
34
34
  const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(where);
35
35
  return {
@@ -43,12 +43,24 @@ var DrizzleMemberRepository = class {
43
43
  if (orgUserIds.length === 0) return [];
44
44
  return this.db.select(personColumns(orgId)).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
45
45
  }
46
- async spaceMemberCounts(orgId, communityId) {
46
+ eventAttendees(orgId, eventId, page) {
47
+ return eventAttendeePage(this.db, orgId, eventId, page);
48
+ }
49
+ eventFaces(orgId, eventIds, limit) {
50
+ return eventFacesFor(this.db, orgId, eventIds, limit);
51
+ }
52
+ directEventGrants(orgId, orgUserId, eventIds) {
53
+ return directEventGrantIds(this.db, orgId, orgUserId, eventIds);
54
+ }
55
+ events(orgId, query, reader, now) {
56
+ return memberEventIds(this.db, orgId, query, reader, now);
57
+ }
58
+ async spaceMemberCounts(orgId, communityId, excluding) {
47
59
  const holders = this.db.$with("holders").as(this.db.selectDistinct({ orgUserId: contentAccess.orgUserId }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId), communityPublished(sql`${orgId}`, sql`${communityId}`))));
48
60
  const seated = this.db.$with("seated").as(this.db.select({
49
61
  spaceId: spaceMembers.spaceId,
50
62
  count: sql`cast(count(*) as int)`.as("count")
51
- }).from(spaceMembers).innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId)).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.communityId, communityId), eq(spaceMembers.status, "active"))).groupBy(spaceMembers.spaceId));
63
+ }).from(spaceMembers).innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId)).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.communityId, communityId), eq(spaceMembers.status, "active"), excluding.length > 0 ? sql`not ${oneOf(sql`${spaceMembers.orgUserId}`, excluding)}` : void 0)).groupBy(spaceMembers.spaceId));
52
64
  const rows = await this.db.with(holders, seated).select({
53
65
  spaceId: spaces.id,
54
66
  count: sql`case when ${spaces.accessRule} = 'all_members' then (select cast(count(*) as int) from ${holders}) else coalesce(${sql.identifier("seated")}.${sql.identifier("count")}, 0) end`
@@ -1 +1 @@
1
- {"version":3,"file":"member.js","names":[],"sources":["../../src/repositories/member.ts"],"sourcesContent":["// member — Drizzle read repo (implements the reporting/member outbound port).\nimport { and, asc, eq, ilike, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DirectoryQuery,\n DirectoryRow,\n MemberReportRepository,\n} from \"@headless-lms/core/reporting/member\";\nimport type { Page } from \"@headless-lms/core/types\";\nimport {\n contentAccess,\n orgUsers,\n spaceMembers,\n spaces,\n userProfiles,\n users,\n} from \"../schema/index.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { communityMember, communityPublished } from \"./grant-covers.js\";\n\nfunction personColumns(orgId: string) {\n return {\n orgUserId: orgUsers.id,\n userId: orgUsers.userId,\n role: orgUsers.role,\n displayName: sql<\n string | null\n >`coalesce(nullif(trim(${userProfiles.displayName}), ''), nullif(trim(concat_ws(' ', ${users.firstName}, ${users.lastName})), ''))`,\n headline: userProfiles.headline,\n bio: userProfiles.bio,\n imageAssetId: sql<\n string | null\n >`case when ${userProfiles.imageAssetOrgId} = ${orgId} then ${userProfiles.imageAssetId} end`,\n };\n}\n\nexport class DrizzleMemberRepository implements MemberReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async directory(\n orgId: string,\n communityId: string,\n query: DirectoryQuery,\n ): Promise<Page<DirectoryRow>> {\n const columns = personColumns(orgId);\n const q = query.search?.trim();\n const where = and(\n eq(orgUsers.orgId, orgId),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n q ? ilike(columns.displayName, `%${q}%`) : undefined,\n );\n const rows = await this.db\n .select(columns)\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where)\n .orderBy(asc(userProfiles.displayName), asc(orgUsers.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(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n return { rows, total, page: query.page, pageSize: query.pageSize };\n }\n\n async people(orgId: string, orgUserIds: string[]): Promise<DirectoryRow[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n return this.db\n .select(personColumns(orgId))\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n }\n\n async spaceMemberCounts(orgId: string, communityId: string): Promise<Record<string, number>> {\n const holders = this.db.$with(\"holders\").as(\n this.db\n .selectDistinct({ orgUserId: contentAccess.orgUserId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.contentId, communityId),\n communityPublished(sql`${orgId}`, sql`${communityId}`),\n ),\n ),\n );\n const seated = this.db.$with(\"seated\").as(\n this.db\n .select({\n spaceId: spaceMembers.spaceId,\n count: sql<number>`cast(count(*) as int)`.as(\"count\"),\n })\n .from(spaceMembers)\n .innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId))\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.communityId, communityId),\n eq(spaceMembers.status, \"active\"),\n ),\n )\n .groupBy(spaceMembers.spaceId),\n );\n const rows = await this.db\n .with(holders, seated)\n .select({\n spaceId: spaces.id,\n count: sql<number>`case when ${spaces.accessRule} = 'all_members' then (select cast(count(*) as int) from ${holders}) else coalesce(${sql.identifier(\"seated\")}.${sql.identifier(\"count\")}, 0) end`,\n })\n .from(spaces)\n .leftJoin(seated, eq(seated.spaceId, spaces.id))\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)));\n return Object.fromEntries(rows.map((row) => [row.spaceId, Number(row.count)]));\n }\n}\ntranslateDbErrors(DrizzleMemberRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"member.js","names":[],"sources":["../../src/repositories/member.ts"],"sourcesContent":["// member — Drizzle read repo (implements the reporting/member outbound port).\nimport { and, asc, eq, ilike, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DirectoryQuery,\n DirectoryRow,\n EventAttendeeRow,\n EventReader,\n MemberReportRepository,\n} from \"@headless-lms/core/reporting/member\";\nimport type { MemberEventsQuery, Page } from \"@headless-lms/core/types\";\nimport type { EventFaces } from \"@headless-lms/core/reporting/events\";\nimport {\n contentAccess,\n orgUsers,\n spaceMembers,\n spaces,\n userProfiles,\n users,\n} from \"../schema/index.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { communityMember, communityPublished, oneOf } from \"./grant-covers.js\";\nimport {\n directEventGrantIds,\n eventAttendeePage,\n eventFacesFor,\n memberEventIds,\n} from \"./event-reports.js\";\n\nfunction personColumns(orgId: string) {\n return {\n orgUserId: orgUsers.id,\n userId: orgUsers.userId,\n displayName: sql<\n string | null\n >`coalesce(nullif(trim(${userProfiles.displayName}), ''), nullif(trim(concat_ws(' ', ${users.firstName}, ${users.lastName})), ''))`,\n headline: userProfiles.headline,\n bio: userProfiles.bio,\n imageAssetId: sql<\n string | null\n >`case when ${userProfiles.imageAssetOrgId} = ${orgId} then ${userProfiles.imageAssetId} end`,\n };\n}\n\nexport class DrizzleMemberRepository implements MemberReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async directory(\n orgId: string,\n communityId: string,\n query: DirectoryQuery,\n readerIds: string[],\n ): Promise<Page<DirectoryRow>> {\n const columns = personColumns(orgId);\n const q = query.search?.trim();\n const where = and(\n eq(orgUsers.orgId, orgId),\n communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${communityId}`, readerIds),\n q ? ilike(columns.displayName, `%${q}%`) : undefined,\n );\n const rows = await this.db\n .select(columns)\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where)\n .orderBy(asc(userProfiles.displayName), asc(orgUsers.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(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n return { rows, total, page: query.page, pageSize: query.pageSize };\n }\n\n async people(orgId: string, orgUserIds: string[]): Promise<DirectoryRow[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n return this.db\n .select(personColumns(orgId))\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n }\n\n eventAttendees(\n orgId: string,\n eventId: string,\n page: { page: number; pageSize: number },\n ): Promise<Page<EventAttendeeRow>> {\n return eventAttendeePage(this.db, orgId, eventId, page);\n }\n\n eventFaces(\n orgId: string,\n eventIds: string[],\n limit: number,\n ): Promise<Record<string, EventFaces>> {\n return eventFacesFor(this.db, orgId, eventIds, limit);\n }\n\n directEventGrants(orgId: string, orgUserId: string, eventIds: string[]): Promise<string[]> {\n return directEventGrantIds(this.db, orgId, orgUserId, eventIds);\n }\n\n events(\n orgId: string,\n query: MemberEventsQuery,\n reader: EventReader,\n now: Date,\n ): Promise<Page<string>> {\n return memberEventIds(this.db, orgId, query, reader, now);\n }\n\n async spaceMemberCounts(\n orgId: string,\n communityId: string,\n excluding: string[],\n ): Promise<Record<string, number>> {\n const holders = this.db.$with(\"holders\").as(\n this.db\n .selectDistinct({ orgUserId: contentAccess.orgUserId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.contentId, communityId),\n communityPublished(sql`${orgId}`, sql`${communityId}`),\n ),\n ),\n );\n const seated = this.db.$with(\"seated\").as(\n this.db\n .select({\n spaceId: spaceMembers.spaceId,\n count: sql<number>`cast(count(*) as int)`.as(\"count\"),\n })\n .from(spaceMembers)\n .innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId))\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.communityId, communityId),\n eq(spaceMembers.status, \"active\"),\n excluding.length > 0\n ? sql`not ${oneOf(sql`${spaceMembers.orgUserId}`, excluding)}`\n : undefined,\n ),\n )\n .groupBy(spaceMembers.spaceId),\n );\n const rows = await this.db\n .with(holders, seated)\n .select({\n spaceId: spaces.id,\n count: sql<number>`case when ${spaces.accessRule} = 'all_members' then (select cast(count(*) as int) from ${holders}) else coalesce(${sql.identifier(\"seated\")}.${sql.identifier(\"count\")}, 0) end`,\n })\n .from(spaces)\n .leftJoin(seated, eq(seated.spaceId, spaces.id))\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)));\n return Object.fromEntries(rows.map((row) => [row.spaceId, Number(row.count)]));\n }\n}\ntranslateDbErrors(DrizzleMemberRepository);\n"],"mappings":"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"}
@@ -1,15 +1,12 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Member, MemberRecord, MembersQuery, MembersRepository, Page } from "@headless-lms/core/organizations";
2
+ import { MembersRepository, TeamRow } from "@headless-lms/core/organizations";
3
3
  import { Logger } from "@headless-lms/core/shared/ports";
4
4
  //#region src/repositories/members.d.ts
5
5
  declare class DrizzleMembersRepository implements MembersRepository {
6
6
  private readonly db;
7
7
  private readonly logger;
8
8
  constructor(db: NodePgDatabase, logger?: Logger);
9
- private loadAll;
10
- list(orgId: string, query: MembersQuery): Promise<Page<Member>>;
11
- findByEmail(orgId: string, email: string): Promise<MemberRecord | null>;
12
- findById(orgId: string, id: string): Promise<MemberRecord | null>;
9
+ teamRows(orgId: string, memberRoleKey: string): Promise<TeamRow[]>;
13
10
  }
14
11
  //#endregion
15
12
  export { DrizzleMembersRepository };
@@ -1,26 +1,12 @@
1
1
  import { users } from "../schema/identity.js";
2
2
  import { invites, orgUsers } from "../schema/organizations.js";
3
+ import { roleAssignments } from "../schema/roles.js";
3
4
  import { translateDbErrors } from "./pg-errors.js";
4
5
  import { user } from "../schema/better-auth.js";
5
6
  import { orgUserProfileColumns } from "./org-user-profile.js";
6
- import { and, eq, ne } from "drizzle-orm";
7
7
  import { noopLogger } from "@headless-lms/core/shared/logger";
8
- import { STUDENT_ROLE, isRole } from "@headless-lms/core/organizations";
8
+ import { and, eq, inArray, isNotNull, or, sql } from "drizzle-orm";
9
9
  //#region src/repositories/members.ts
10
- const roleOf = (t) => isRole(t) && t !== STUDENT_ROLE ? t : "instructor";
11
- function toMember(r) {
12
- return {
13
- id: r.id,
14
- firstName: r.firstName,
15
- lastName: r.lastName,
16
- email: r.email,
17
- image: r.image,
18
- role: r.role,
19
- status: r.status,
20
- joinedAt: r.joinedAt,
21
- invitedAt: r.invitedAt
22
- };
23
- }
24
10
  var DrizzleMembersRepository = class {
25
11
  db;
26
12
  logger;
@@ -28,87 +14,46 @@ var DrizzleMembersRepository = class {
28
14
  this.db = db;
29
15
  this.logger = logger;
30
16
  }
31
- async loadAll(orgId) {
32
- const memberRows = await this.db.select({
33
- ...orgUserProfileColumns,
34
- role: orgUsers.role,
35
- joinedAt: orgUsers.createdAt,
36
- userExternalId: users.externalId
37
- }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), ne(orgUsers.role, STUDENT_ROLE)));
38
- const inviteRows = await this.db.select({
39
- id: invites.id,
17
+ async teamRows(orgId, memberRoleKey) {
18
+ const pendingInvite = this.db.select({
40
19
  email: invites.email,
41
- role: invites.role,
20
+ roleKey: invites.roleKey,
42
21
  invitedAt: invites.createdAt
43
- }).from(invites).where(and(eq(invites.orgId, orgId), eq(invites.status, "pending"), ne(invites.role, STUDENT_ROLE)));
44
- const members = memberRows.map((m) => ({
45
- id: m.id,
46
- firstName: m.firstName,
47
- lastName: m.lastName,
48
- email: m.email,
49
- image: m.image ?? null,
50
- role: roleOf(m.role),
51
- status: "active",
52
- joinedAt: m.joinedAt.toISOString(),
53
- invitedAt: null,
54
- kind: "member",
55
- userExternalId: m.userExternalId,
56
- inviteId: null
57
- }));
58
- const invited = inviteRows.map((i) => ({
59
- id: i.id,
60
- firstName: null,
61
- lastName: null,
62
- email: i.email,
63
- image: null,
64
- role: roleOf(i.role),
65
- status: "invited",
66
- joinedAt: null,
67
- invitedAt: i.invitedAt.toISOString(),
68
- kind: "invite",
69
- userExternalId: null,
70
- inviteId: i.id
71
- }));
72
- return [...members, ...invited];
73
- }
74
- async list(orgId, query) {
75
- let rows = await this.loadAll(orgId);
76
- if (query.role) rows = rows.filter((r) => r.role === query.role);
77
- if (query.status) rows = rows.filter((r) => r.status === query.status);
78
- const q = query.search?.trim().toLowerCase();
79
- if (q) rows = rows.filter((r) => [
80
- r.firstName,
81
- r.lastName,
82
- r.email
83
- ].some((v) => v?.toLowerCase().includes(q)));
84
- const sort = query.sort;
85
- const desc = sort?.startsWith("-") ?? false;
86
- const key = desc ? sort.slice(1) : sort;
87
- const byName = (a, b) => a === b ? 0 : a === null ? 1 : b === null ? -1 : a.localeCompare(b);
88
- rows.sort((a, b) => {
89
- if (key === "email") return desc ? -a.email.localeCompare(b.email) : a.email.localeCompare(b.email);
90
- if (key === "role") return desc ? -a.role.localeCompare(b.role) : a.role.localeCompare(b.role);
91
- if (key === "lastName") {
92
- const cmp = byName(a.lastName, b.lastName) || byName(a.firstName, b.firstName);
93
- return desc ? -cmp : cmp;
94
- }
95
- const cmp = byName(a.firstName, b.firstName) || byName(a.lastName, b.lastName);
96
- return desc ? -cmp : cmp;
22
+ }).from(invites).where(and(eq(invites.orgId, orgId), eq(invites.status, "pending"))).as("pending_invite");
23
+ const rows = await this.db.select({
24
+ ...orgUserProfileColumns,
25
+ status: orgUsers.status,
26
+ joinedAt: orgUsers.createdAt,
27
+ inviteRoleKey: pendingInvite.roleKey,
28
+ invitedAt: pendingInvite.invitedAt
29
+ }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).leftJoin(pendingInvite, sql`lower(${pendingInvite.email}) = lower(${users.email})`).where(and(eq(orgUsers.orgId, orgId), or(sql`exists (select 1 from ${roleAssignments} ra where ra.org_id = ${orgUsers.orgId} and ra.org_user_id = ${orgUsers.id})`, and(eq(orgUsers.status, "invited"), isNotNull(pendingInvite.roleKey), sql`${pendingInvite.roleKey} <> ${memberRoleKey}`))));
30
+ if (rows.length === 0) return [];
31
+ const assignments = await this.db.select({
32
+ orgUserId: roleAssignments.orgUserId,
33
+ roleKey: roleAssignments.roleKey,
34
+ contentId: roleAssignments.contentId,
35
+ contentType: roleAssignments.contentType
36
+ }).from(roleAssignments).where(and(eq(roleAssignments.orgId, orgId), inArray(roleAssignments.orgUserId, rows.map((row) => row.id))));
37
+ return rows.map((row) => {
38
+ const own = assignments.filter((a) => a.orgUserId === row.id);
39
+ return {
40
+ id: row.id,
41
+ firstName: row.firstName,
42
+ lastName: row.lastName,
43
+ email: row.email,
44
+ image: row.image ?? null,
45
+ status: row.status,
46
+ joinedAt: row.joinedAt,
47
+ invitedAt: row.invitedAt ?? null,
48
+ orgRoleKey: own.find((a) => a.contentId === null)?.roleKey ?? null,
49
+ inviteRoleKey: row.inviteRoleKey ?? null,
50
+ items: own.filter((a) => a.contentId !== null && a.contentType !== null).map((a) => ({
51
+ contentId: a.contentId,
52
+ contentType: a.contentType,
53
+ roleKey: a.roleKey
54
+ }))
55
+ };
97
56
  });
98
- const total = rows.length;
99
- const start = (query.page - 1) * query.pageSize;
100
- return {
101
- rows: rows.slice(start, start + query.pageSize).map(toMember),
102
- total,
103
- page: query.page,
104
- pageSize: query.pageSize
105
- };
106
- }
107
- async findByEmail(orgId, email) {
108
- return (await this.loadAll(orgId)).find((r) => r.email.toLowerCase() === email.toLowerCase()) ?? null;
109
- }
110
- async findById(orgId, id) {
111
- return (await this.loadAll(orgId)).find((r) => r.id === id) ?? null;
112
57
  }
113
58
  };
114
59
  translateDbErrors(DrizzleMembersRepository);
@@ -1 +1 @@
1
- {"version":3,"file":"members.js","names":[],"sources":["../../src/repositories/members.ts"],"sourcesContent":["// organizations members — Drizzle read repository. Reads the domain mirror of the\n// org's members (orgUsers) and pending invites, joined to the identity user for\n// display. Writes go through the auth provider (see adapters/auth/org-admin.ts).\nimport { and, eq, ne } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n isRole,\n STUDENT_ROLE,\n type Member,\n type MemberRecord,\n type MembersQuery,\n type MembersRepository,\n type Page,\n type TeamRole,\n} from \"@headless-lms/core/organizations\";\nimport { orgUsers, invites } from \"../schema/organizations.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { orgUserProfileColumns } from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst roleOf = (t: string): TeamRole => (isRole(t) && t !== STUDENT_ROLE ? t : \"instructor\");\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport class DrizzleMembersRepository implements MembersRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private async loadAll(orgId: string): Promise<MemberRecord[]> {\n // Learners share this table now, so the role filter is what keeps them off\n // the staff list. Name/email come from the identity user the row links to.\n const memberRows = await this.db\n .select({\n ...orgUserProfileColumns,\n role: orgUsers.role,\n joinedAt: orgUsers.createdAt,\n userExternalId: users.externalId,\n })\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), ne(orgUsers.role, STUDENT_ROLE)));\n\n // Student invites live in the same table but belong to the students\n // surface, not the members list.\n const inviteRows = await this.db\n .select({\n id: invites.id,\n email: invites.email,\n role: invites.role,\n invitedAt: invites.createdAt,\n })\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n eq(invites.status, \"pending\"),\n ne(invites.role, STUDENT_ROLE),\n ),\n );\n\n const members: MemberRecord[] = memberRows.map((m) => ({\n id: m.id,\n firstName: m.firstName,\n lastName: m.lastName,\n email: m.email,\n image: m.image ?? null,\n role: roleOf(m.role),\n status: \"active\",\n joinedAt: m.joinedAt.toISOString(),\n invitedAt: null,\n kind: \"member\",\n userExternalId: m.userExternalId,\n inviteId: null,\n }));\n // A pending invite is an address and nothing else — the invites table\n // carries no names, so there is nobody to name until they accept.\n const invited: MemberRecord[] = inviteRows.map((i) => ({\n id: i.id,\n firstName: null,\n lastName: null,\n email: i.email,\n image: null,\n role: roleOf(i.role),\n status: \"invited\",\n joinedAt: null,\n invitedAt: i.invitedAt.toISOString(),\n kind: \"invite\",\n userExternalId: null,\n inviteId: i.id,\n }));\n return [...members, ...invited];\n }\n\n async list(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n let rows = await this.loadAll(orgId);\n if (query.role) {\n rows = rows.filter((r) => r.role === query.role);\n }\n if (query.status) {\n rows = rows.filter((r) => r.status === query.status);\n }\n const q = query.search?.trim().toLowerCase();\n if (q) {\n rows = rows.filter((r) =>\n [r.firstName, r.lastName, r.email].some((v) => v?.toLowerCase().includes(q)),\n );\n }\n\n const sort = query.sort;\n const desc = sort?.startsWith(\"-\") ?? false;\n const key = (desc ? sort!.slice(1) : sort) as keyof Member | undefined;\n // Nulls last whichever way the column is sorted — an unaccepted invite has\n // no name and belongs at the bottom, not interleaved.\n const byName = (a: string | null, b: string | null): number =>\n a === b ? 0 : a === null ? 1 : b === null ? -1 : a.localeCompare(b);\n rows.sort((a, b) => {\n if (key === \"email\") {\n return desc ? -a.email.localeCompare(b.email) : a.email.localeCompare(b.email);\n }\n if (key === \"role\") {\n return desc ? -a.role.localeCompare(b.role) : a.role.localeCompare(b.role);\n }\n if (key === \"lastName\") {\n const cmp = byName(a.lastName, b.lastName) || byName(a.firstName, b.firstName);\n return desc ? -cmp : cmp;\n }\n const cmp = byName(a.firstName, b.firstName) || byName(a.lastName, b.lastName);\n return desc ? -cmp : cmp;\n });\n\n const total = rows.length;\n const start = (query.page - 1) * query.pageSize;\n return {\n rows: rows.slice(start, start + query.pageSize).map(toMember),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findByEmail(orgId: string, email: string): Promise<MemberRecord | null> {\n const all = await this.loadAll(orgId);\n return all.find((r) => r.email.toLowerCase() === email.toLowerCase()) ?? null;\n }\n\n async findById(orgId: string, id: string): Promise<MemberRecord | null> {\n const all = await this.loadAll(orgId);\n return all.find((r) => r.id === id) ?? null;\n }\n}\ntranslateDbErrors(DrizzleMembersRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"members.js","names":[],"sources":["../../src/repositories/members.ts"],"sourcesContent":["import { and, eq, inArray, isNotNull, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { MembersRepository, TeamRow } from \"@headless-lms/core/organizations\";\nimport type { ContentType } from \"@headless-lms/core/types\";\nimport { orgUsers, invites } from \"../schema/organizations.js\";\nimport { roleAssignments } from \"../schema/roles.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { orgUserProfileColumns } from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nexport class DrizzleMembersRepository implements MembersRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async teamRows(orgId: string, memberRoleKey: string): Promise<TeamRow[]> {\n const pendingInvite = this.db\n .select({\n email: invites.email,\n roleKey: invites.roleKey,\n invitedAt: invites.createdAt,\n })\n .from(invites)\n .where(and(eq(invites.orgId, orgId), eq(invites.status, \"pending\")))\n .as(\"pending_invite\");\n const rows = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n joinedAt: orgUsers.createdAt,\n inviteRoleKey: pendingInvite.roleKey,\n invitedAt: pendingInvite.invitedAt,\n })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .leftJoin(pendingInvite, sql`lower(${pendingInvite.email}) = lower(${users.email})`)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n or(\n sql`exists (select 1 from ${roleAssignments} ra where ra.org_id = ${orgUsers.orgId} and ra.org_user_id = ${orgUsers.id})`,\n and(\n eq(orgUsers.status, \"invited\"),\n isNotNull(pendingInvite.roleKey),\n sql`${pendingInvite.roleKey} <> ${memberRoleKey}`,\n ),\n ),\n ),\n );\n if (rows.length === 0) {\n return [];\n }\n const assignments = await this.db\n .select({\n orgUserId: roleAssignments.orgUserId,\n roleKey: roleAssignments.roleKey,\n contentId: roleAssignments.contentId,\n contentType: roleAssignments.contentType,\n })\n .from(roleAssignments)\n .where(\n and(\n eq(roleAssignments.orgId, orgId),\n inArray(\n roleAssignments.orgUserId,\n rows.map((row) => row.id),\n ),\n ),\n );\n return rows.map((row) => {\n const own = assignments.filter((a) => a.orgUserId === row.id);\n return {\n id: row.id,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email,\n image: row.image ?? null,\n status: row.status,\n joinedAt: row.joinedAt,\n invitedAt: row.invitedAt ?? null,\n orgRoleKey: own.find((a) => a.contentId === null)?.roleKey ?? null,\n inviteRoleKey: row.inviteRoleKey ?? null,\n items: own\n .filter((a) => a.contentId !== null && a.contentType !== null)\n .map((a) => ({\n contentId: a.contentId!,\n contentType: a.contentType as ContentType,\n roleKey: a.roleKey,\n })),\n };\n });\n }\n}\ntranslateDbErrors(DrizzleMembersRepository);\n"],"mappings":";;;;;;;;;AAaA,IAAa,2BAAb,MAAmE;CAE9C;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,SAAS,OAAe,eAA2C;EACvE,MAAM,gBAAgB,KAAK,GACxB,OAAO;GACN,OAAO,QAAQ;GACf,SAAS,QAAQ;GACjB,WAAW,QAAQ;EACrB,CAAC,CAAC,CACD,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC,CACnE,GAAG,gBAAgB;EACtB,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GACN,GAAG;GACH,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,eAAe,cAAc;GAC7B,WAAW,cAAc;EAC3B,CAAC,CAAC,CACD,KAAK,QAAQ,CAAC,CACd,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,SAAS,MAAM,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC,CAAC,CAC7C,SAAS,eAAe,GAAG,SAAS,cAAc,MAAM,YAAY,MAAM,MAAM,EAAE,CAAC,CACnF,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,GACE,GAAG,yBAAyB,gBAAgB,wBAAwB,SAAS,MAAM,wBAAwB,SAAS,GAAG,IACvH,IACE,GAAG,SAAS,QAAQ,SAAS,GAC7B,UAAU,cAAc,OAAO,GAC/B,GAAG,GAAG,cAAc,QAAQ,MAAM,eACpC,CACF,CACF,CACF;EACF,IAAI,KAAK,WAAW,GAClB,OAAO,CAAC;EAEV,MAAM,cAAc,MAAM,KAAK,GAC5B,OAAO;GACN,WAAW,gBAAgB;GAC3B,SAAS,gBAAgB;GACzB,WAAW,gBAAgB;GAC3B,aAAa,gBAAgB;EAC/B,CAAC,CAAC,CACD,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,QACE,gBAAgB,WAChB,KAAK,KAAK,QAAQ,IAAI,EAAE,CAC1B,CACF,CACF;EACF,OAAO,KAAK,KAAK,QAAQ;GACvB,MAAM,MAAM,YAAY,QAAQ,MAAM,EAAE,cAAc,IAAI,EAAE;GAC5D,OAAO;IACL,IAAI,IAAI;IACR,WAAW,IAAI;IACf,UAAU,IAAI;IACd,OAAO,IAAI;IACX,OAAO,IAAI,SAAS;IACpB,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,WAAW,IAAI,aAAa;IAC5B,YAAY,IAAI,MAAM,MAAM,EAAE,cAAc,IAAI,CAAC,EAAE,WAAW;IAC9D,eAAe,IAAI,iBAAiB;IACpC,OAAO,IACJ,QAAQ,MAAM,EAAE,cAAc,QAAQ,EAAE,gBAAgB,IAAI,CAAC,CAC7D,KAAK,OAAO;KACX,WAAW,EAAE;KACb,aAAa,EAAE;KACf,SAAS,EAAE;IACb,EAAE;GACN;EACF,CAAC;CACH;AACF;AACA,kBAAkB,wBAAwB"}
@@ -0,0 +1,17 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, MentionItemType } from "@headless-lms/core/types";
3
+ import { MentionsRepository } from "@headless-lms/core/discussion";
4
+ //#region src/repositories/mentions.d.ts
5
+ declare class DrizzleMentionsRepository implements MentionsRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]>;
10
+ forItems(orgId: string, itemType: MentionItemType, itemIds: string[]): Promise<Record<string, string[]>>;
11
+ sync(orgId: string, itemType: MentionItemType, itemId: string, orgUserIds: string[], at: Date): Promise<string[]>;
12
+ removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void>;
13
+ private item;
14
+ }
15
+ //#endregion
16
+ export { DrizzleMentionsRepository };
17
+ //# sourceMappingURL=mentions.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { mentions } from "../schema/discussion.js";
2
+ import "../schema/index.js";
3
+ import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
5
+ //#region src/repositories/mentions.ts
6
+ var DrizzleMentionsRepository = class {
7
+ db;
8
+ logger;
9
+ constructor(db, logger = noopLogger) {
10
+ this.db = db;
11
+ this.logger = logger;
12
+ }
13
+ async ofItem(orgId, itemType, itemId) {
14
+ return (await this.db.select({ orgUserId: mentions.orgUserId }).from(mentions).where(this.item(orgId, itemType, itemId)).orderBy(asc(mentions.createdAt), asc(mentions.orgUserId))).map((row) => row.orgUserId);
15
+ }
16
+ async forItems(orgId, itemType, itemIds) {
17
+ if (itemIds.length === 0) return {};
18
+ const rows = await this.db.select({
19
+ itemId: mentions.itemId,
20
+ orgUserId: mentions.orgUserId
21
+ }).from(mentions).where(and(eq(mentions.orgId, orgId), eq(mentions.itemType, itemType), inArray(mentions.itemId, itemIds))).orderBy(asc(mentions.itemId), asc(mentions.createdAt), asc(mentions.orgUserId));
22
+ const byItem = {};
23
+ for (const row of rows) (byItem[row.itemId] ??= []).push(row.orgUserId);
24
+ return byItem;
25
+ }
26
+ async sync(orgId, itemType, itemId, orgUserIds, at) {
27
+ const item = this.item(orgId, itemType, itemId);
28
+ await this.db.delete(mentions).where(orgUserIds.length > 0 ? and(item, notInArray(mentions.orgUserId, orgUserIds)) : item);
29
+ if (orgUserIds.length === 0) return [];
30
+ const added = await this.db.insert(mentions).values(orgUserIds.map((orgUserId) => ({
31
+ orgId,
32
+ itemType,
33
+ itemId,
34
+ orgUserId,
35
+ createdAt: at
36
+ }))).onConflictDoNothing().returning({ orgUserId: mentions.orgUserId });
37
+ this.logger.debug("mentions synced", {
38
+ orgId,
39
+ itemType,
40
+ itemId,
41
+ added: added.length
42
+ });
43
+ return added.map((row) => row.orgUserId);
44
+ }
45
+ async removeItem(orgId, itemType, itemId) {
46
+ await this.db.delete(mentions).where(this.item(orgId, itemType, itemId));
47
+ }
48
+ item(orgId, itemType, itemId) {
49
+ return and(eq(mentions.orgId, orgId), eq(mentions.itemType, itemType), eq(mentions.itemId, itemId));
50
+ }
51
+ };
52
+ //#endregion
53
+ export { DrizzleMentionsRepository };
54
+
55
+ //# sourceMappingURL=mentions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mentions.js","names":[],"sources":["../../src/repositories/mentions.ts"],"sourcesContent":["import { and, asc, eq, inArray, notInArray } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { MentionsRepository } from \"@headless-lms/core/discussion\";\nimport type { Logger, MentionItemType } from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { mentions } from \"../schema/index.js\";\n\nexport class DrizzleMentionsRepository implements MentionsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async ofItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<string[]> {\n const rows = await this.db\n .select({ orgUserId: mentions.orgUserId })\n .from(mentions)\n .where(this.item(orgId, itemType, itemId))\n .orderBy(asc(mentions.createdAt), asc(mentions.orgUserId));\n return rows.map((row) => row.orgUserId);\n }\n\n async forItems(\n orgId: string,\n itemType: MentionItemType,\n itemIds: string[],\n ): Promise<Record<string, string[]>> {\n if (itemIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({ itemId: mentions.itemId, orgUserId: mentions.orgUserId })\n .from(mentions)\n .where(\n and(\n eq(mentions.orgId, orgId),\n eq(mentions.itemType, itemType),\n inArray(mentions.itemId, itemIds),\n ),\n )\n .orderBy(asc(mentions.itemId), asc(mentions.createdAt), asc(mentions.orgUserId));\n const byItem: Record<string, string[]> = {};\n for (const row of rows) {\n (byItem[row.itemId] ??= []).push(row.orgUserId);\n }\n return byItem;\n }\n\n async sync(\n orgId: string,\n itemType: MentionItemType,\n itemId: string,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]> {\n const item = this.item(orgId, itemType, itemId);\n await this.db\n .delete(mentions)\n .where(orgUserIds.length > 0 ? and(item, notInArray(mentions.orgUserId, orgUserIds)) : item);\n if (orgUserIds.length === 0) {\n return [];\n }\n const added = await this.db\n .insert(mentions)\n .values(\n orgUserIds.map((orgUserId) => ({ orgId, itemType, itemId, orgUserId, createdAt: at })),\n )\n .onConflictDoNothing()\n .returning({ orgUserId: mentions.orgUserId });\n this.logger.debug(\"mentions synced\", { orgId, itemType, itemId, added: added.length });\n return added.map((row) => row.orgUserId);\n }\n\n async removeItem(orgId: string, itemType: MentionItemType, itemId: string): Promise<void> {\n await this.db.delete(mentions).where(this.item(orgId, itemType, itemId));\n }\n\n private item(orgId: string, itemType: MentionItemType, itemId: string) {\n return and(\n eq(mentions.orgId, orgId),\n eq(mentions.itemType, itemType),\n eq(mentions.itemId, itemId),\n );\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,UAA2B,QAAmC;EAMxF,QAAO,MALY,KAAK,GACrB,OAAO,EAAE,WAAW,SAAS,UAAU,CAAC,CAAC,CACzC,KAAK,QAAQ,CAAC,CACd,MAAM,KAAK,KAAK,OAAO,UAAU,MAAM,CAAC,CAAC,CACzC,QAAQ,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,SAAS,CAAC,EAAA,CAC/C,KAAK,QAAQ,IAAI,SAAS;CACxC;CAEA,MAAM,SACJ,OACA,UACA,SACmC;EACnC,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GAAE,QAAQ,SAAS;GAAQ,WAAW,SAAS;EAAU,CAAC,CAAC,CAClE,KAAK,QAAQ,CAAC,CACd,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,UAAU,QAAQ,GAC9B,QAAQ,SAAS,QAAQ,OAAO,CAClC,CACF,CAAC,CACA,QAAQ,IAAI,SAAS,MAAM,GAAG,IAAI,SAAS,SAAS,GAAG,IAAI,SAAS,SAAS,CAAC;EACjF,MAAM,SAAmC,CAAC;EAC1C,KAAK,MAAM,OAAO,MAChB,CAAC,OAAO,IAAI,YAAY,CAAC,EAAA,CAAG,KAAK,IAAI,SAAS;EAEhD,OAAO;CACT;CAEA,MAAM,KACJ,OACA,UACA,QACA,YACA,IACmB;EACnB,MAAM,OAAO,KAAK,KAAK,OAAO,UAAU,MAAM;EAC9C,MAAM,KAAK,GACR,OAAO,QAAQ,CAAC,CAChB,MAAM,WAAW,SAAS,IAAI,IAAI,MAAM,WAAW,SAAS,WAAW,UAAU,CAAC,IAAI,IAAI;EAC7F,IAAI,WAAW,WAAW,GACxB,OAAO,CAAC;EAEV,MAAM,QAAQ,MAAM,KAAK,GACtB,OAAO,QAAQ,CAAC,CAChB,OACC,WAAW,KAAK,eAAe;GAAE;GAAO;GAAU;GAAQ;GAAW,WAAW;EAAG,EAAE,CACvF,CAAC,CACA,oBAAoB,CAAC,CACrB,UAAU,EAAE,WAAW,SAAS,UAAU,CAAC;EAC9C,KAAK,OAAO,MAAM,mBAAmB;GAAE;GAAO;GAAU;GAAQ,OAAO,MAAM;EAAO,CAAC;EACrF,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS;CACzC;CAEA,MAAM,WAAW,OAAe,UAA2B,QAA+B;EACxF,MAAM,KAAK,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,KAAK,KAAK,OAAO,UAAU,MAAM,CAAC;CACzE;CAEA,KAAa,OAAe,UAA2B,QAAgB;EACrE,OAAO,IACL,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,UAAU,QAAQ,GAC9B,GAAG,SAAS,QAAQ,MAAM,CAC5B;CACF;AACF"}
@@ -0,0 +1,19 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, StoredSetting } from "@headless-lms/core/types";
3
+ import { NotificationSettingsRepository, SettingKey } from "@headless-lms/core/identity";
4
+ //#region src/repositories/notification-settings.d.ts
5
+ declare class DrizzleNotificationSettingsRepository implements NotificationSettingsRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ forUsers(orgId: string, userIds: string[], communityId: string | null, categories: string[] | null): Promise<(StoredSetting & {
10
+ userId: string;
11
+ })[]>;
12
+ apply(orgId: string, userId: string, upserts: (SettingKey & {
13
+ enabled: boolean;
14
+ })[], deletes: SettingKey[], at: Date): Promise<void>;
15
+ switchedOn(orgId: string, communityId: string, category: string, spaceId: string | null): Promise<string[]>;
16
+ }
17
+ //#endregion
18
+ export { DrizzleNotificationSettingsRepository };
19
+ //# sourceMappingURL=notification-settings.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { notificationSettings } from "../schema/identity.js";
2
+ import { orgUsers } from "../schema/organizations.js";
3
+ import "../schema/index.js";
4
+ import { translateDbErrors } from "./pg-errors.js";
5
+ import { noopLogger } from "@headless-lms/core/shared/logger";
6
+ import { and, eq, inArray, sql } from "drizzle-orm";
7
+ import { genId } from "@headless-lms/core/shared/id";
8
+ //#region src/repositories/notification-settings.ts
9
+ const ns = notificationSettings;
10
+ const communityKey = sql`coalesce(${ns.communityId}, '')`;
11
+ const spaceKey = sql`coalesce(${ns.spaceId}, '')`;
12
+ function matchesKey(key) {
13
+ return sql`(${communityKey} = ${key.communityId ?? ""} and ${spaceKey} = ${key.spaceId ?? ""} and ${ns.category} = ${key.category} and ${ns.channel} = ${key.channel})`;
14
+ }
15
+ var DrizzleNotificationSettingsRepository = class {
16
+ db;
17
+ logger;
18
+ constructor(db, logger = noopLogger) {
19
+ this.db = db;
20
+ this.logger = logger;
21
+ }
22
+ async forUsers(orgId, userIds, communityId, categories) {
23
+ if (userIds.length === 0) return [];
24
+ const conditions = [
25
+ eq(ns.orgId, orgId),
26
+ inArray(ns.userId, userIds),
27
+ sql`${communityKey} = ${communityId ?? ""}`
28
+ ];
29
+ if (categories !== null) {
30
+ if (categories.length === 0) return [];
31
+ conditions.push(inArray(ns.category, categories));
32
+ }
33
+ return this.db.select({
34
+ userId: ns.userId,
35
+ communityId: ns.communityId,
36
+ spaceId: ns.spaceId,
37
+ category: ns.category,
38
+ channel: ns.channel,
39
+ enabled: ns.enabled
40
+ }).from(ns).where(and(...conditions));
41
+ }
42
+ async apply(orgId, userId, upserts, deletes, at) {
43
+ if (upserts.length === 0 && deletes.length === 0) return;
44
+ await this.db.transaction(async (tx) => {
45
+ if (deletes.length > 0) await tx.delete(ns).where(and(eq(ns.orgId, orgId), eq(ns.userId, userId), sql`(${sql.join(deletes.map(matchesKey), sql` or `)})`));
46
+ if (upserts.length > 0) {
47
+ const rows = upserts.map((row) => sql`(${orgId}, ${genId("notificationSetting")}, ${userId}, ${row.communityId}, ${row.spaceId}, ${row.category}, ${row.channel}, ${row.enabled}, ${at.toISOString()}::timestamptz)`);
48
+ await tx.execute(sql`insert into notification_settings (org_id, id, user_id, community_id, space_id, category, channel, enabled, updated_at) values ${sql.join(rows, sql`, `)} on conflict (org_id, user_id, coalesce(community_id, ''), coalesce(space_id, ''), category, channel) do update set enabled = excluded.enabled, updated_at = excluded.updated_at`);
49
+ }
50
+ });
51
+ this.logger.debug("notification settings applied", {
52
+ orgId,
53
+ userId,
54
+ upserts: upserts.length,
55
+ deletes: deletes.length
56
+ });
57
+ }
58
+ async switchedOn(orgId, communityId, category, spaceId) {
59
+ const levels = spaceId === null ? [""] : [spaceId, ""];
60
+ return (await this.db.selectDistinct({ orgUserId: orgUsers.id }).from(ns).innerJoin(orgUsers, and(eq(orgUsers.orgId, ns.orgId), eq(orgUsers.userId, ns.userId))).where(and(eq(ns.orgId, orgId), sql`${communityKey} = ${communityId}`, eq(ns.category, category), sql`${spaceKey} in (${sql.join(levels.map((level) => sql`${level}`), sql`, `)})`, sql`${ns.enabled}`))).map((row) => row.orgUserId);
61
+ }
62
+ };
63
+ translateDbErrors(DrizzleNotificationSettingsRepository);
64
+ //#endregion
65
+ export { DrizzleNotificationSettingsRepository };
66
+
67
+ //# sourceMappingURL=notification-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-settings.js","names":[],"sources":["../../src/repositories/notification-settings.ts"],"sourcesContent":["import { and, eq, inArray, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { NotificationSettingsRepository, SettingKey } from \"@headless-lms/core/identity\";\nimport type { Logger, StoredSetting } from \"@headless-lms/core/types\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { notificationSettings, orgUsers } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst ns = notificationSettings;\nconst communityKey = sql`coalesce(${ns.communityId}, '')`;\nconst spaceKey = sql`coalesce(${ns.spaceId}, '')`;\n\nfunction matchesKey(key: SettingKey): SQL {\n return sql`(${communityKey} = ${key.communityId ?? \"\"} and ${spaceKey} = ${key.spaceId ?? \"\"} and ${ns.category} = ${key.category} and ${ns.channel} = ${key.channel})`;\n}\n\nexport class DrizzleNotificationSettingsRepository implements NotificationSettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async forUsers(\n orgId: string,\n userIds: string[],\n communityId: string | null,\n categories: string[] | null,\n ): Promise<(StoredSetting & { userId: string })[]> {\n if (userIds.length === 0) {\n return [];\n }\n const conditions = [\n eq(ns.orgId, orgId),\n inArray(ns.userId, userIds),\n sql`${communityKey} = ${communityId ?? \"\"}`,\n ];\n if (categories !== null) {\n if (categories.length === 0) {\n return [];\n }\n conditions.push(inArray(ns.category, categories));\n }\n return this.db\n .select({\n userId: ns.userId,\n communityId: ns.communityId,\n spaceId: ns.spaceId,\n category: ns.category,\n channel: ns.channel,\n enabled: ns.enabled,\n })\n .from(ns)\n .where(and(...conditions));\n }\n\n async apply(\n orgId: string,\n userId: string,\n upserts: (SettingKey & { enabled: boolean })[],\n deletes: SettingKey[],\n at: Date,\n ): Promise<void> {\n if (upserts.length === 0 && deletes.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n if (deletes.length > 0) {\n await tx\n .delete(ns)\n .where(\n and(\n eq(ns.orgId, orgId),\n eq(ns.userId, userId),\n sql`(${sql.join(deletes.map(matchesKey), sql` or `)})`,\n ),\n );\n }\n if (upserts.length > 0) {\n const rows = upserts.map(\n (row) =>\n sql`(${orgId}, ${genId(\"notificationSetting\")}, ${userId}, ${row.communityId}, ${row.spaceId}, ${row.category}, ${row.channel}, ${row.enabled}, ${at.toISOString()}::timestamptz)`,\n );\n await tx.execute(\n sql`insert into notification_settings (org_id, id, user_id, community_id, space_id, category, channel, enabled, updated_at) values ${sql.join(rows, sql`, `)} on conflict (org_id, user_id, coalesce(community_id, ''), coalesce(space_id, ''), category, channel) do update set enabled = excluded.enabled, updated_at = excluded.updated_at`,\n );\n }\n });\n this.logger.debug(\"notification settings applied\", {\n orgId,\n userId,\n upserts: upserts.length,\n deletes: deletes.length,\n });\n }\n\n async switchedOn(\n orgId: string,\n communityId: string,\n category: string,\n spaceId: string | null,\n ): Promise<string[]> {\n const levels = spaceId === null ? [\"\"] : [spaceId, \"\"];\n const rows = await this.db\n .selectDistinct({ orgUserId: orgUsers.id })\n .from(ns)\n .innerJoin(orgUsers, and(eq(orgUsers.orgId, ns.orgId), eq(orgUsers.userId, ns.userId)))\n .where(\n and(\n eq(ns.orgId, orgId),\n sql`${communityKey} = ${communityId}`,\n eq(ns.category, category),\n sql`${spaceKey} in (${sql.join(\n levels.map((level) => sql`${level}`),\n sql`, `,\n )})`,\n sql`${ns.enabled}`,\n ),\n );\n return rows.map((row) => row.orgUserId);\n }\n}\ntranslateDbErrors(DrizzleNotificationSettingsRepository);\n"],"mappings":"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"}
@@ -1,22 +1,29 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Notification, NotificationPreference } from "@headless-lms/core/types";
3
- import { NotificationsQuery, NotificationsRepository } from "@headless-lms/core/identity";
1
+ import { DbExecutor } from "../client.js";
2
+ import { Delivery, NotificationFilter, StoredNotification } from "@headless-lms/core/types";
3
+ import { ListedNotification, NewDelivery, NewNotification, NotificationPage, NotificationsRepository, UnreadCounts } from "@headless-lms/core/automations";
4
4
  //#region src/repositories/notifications.d.ts
5
5
  declare class DrizzleNotificationsRepository implements NotificationsRepository {
6
6
  private readonly db;
7
- constructor(db: NodePgDatabase);
8
- insert(orgId: string, rows: Notification[]): Promise<void>;
9
- page(orgId: string, userId: string, query: NotificationsQuery): Promise<{
10
- rows: Notification[];
11
- total: number;
12
- unreadCount: number;
13
- }>;
14
- markRead(orgId: string, userId: string, notificationId: string, at: Date): Promise<Notification | null>;
15
- markAllRead(orgId: string, userId: string, at: Date): Promise<number>;
16
- preferences(orgId: string, userIds: string[]): Promise<(NotificationPreference & {
17
- userId: string;
18
- })[]>;
19
- upsertPreferences(orgId: string, userId: string, preferences: NotificationPreference[]): Promise<void>;
7
+ constructor(db: DbExecutor);
8
+ insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void>;
9
+ pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{
10
+ id: string;
11
+ }[]>;
12
+ findDelivery(orgId: string, deliveryId: string): Promise<{
13
+ delivery: Delivery;
14
+ notification: StoredNotification;
15
+ } | null>;
16
+ markSent(orgId: string, deliveryId: string, at: Date): Promise<void>;
17
+ recordError(orgId: string, deliveryId: string, error: string): Promise<void>;
18
+ markFailed(orgId: string, deliveryId: string, error: string): Promise<void>;
19
+ page(orgId: string, userId: string, filter: NotificationFilter, page: {
20
+ page: number;
21
+ pageSize: number;
22
+ }): Promise<NotificationPage>;
23
+ find(orgId: string, userId: string, notificationId: string): Promise<ListedNotification | null>;
24
+ unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts>;
25
+ markRead(orgId: string, userId: string, notificationId: string, at: Date): Promise<boolean>;
26
+ markAllRead(orgId: string, userId: string, filter: NotificationFilter, at: Date): Promise<number>;
20
27
  }
21
28
  //#endregion
22
29
  export { DrizzleNotificationsRepository };