@headless-lms/adapter-db 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/index.d.ts +12 -6
  2. package/dist/index.js +12 -6
  3. package/dist/repositories/admin.d.ts +23 -0
  4. package/dist/repositories/admin.js +393 -0
  5. package/dist/repositories/admin.js.map +1 -0
  6. package/dist/repositories/assets.d.ts +6 -3
  7. package/dist/repositories/assets.js +46 -9
  8. package/dist/repositories/assets.js.map +1 -1
  9. package/dist/repositories/automations.d.ts +5 -2
  10. package/dist/repositories/automations.js +29 -2
  11. package/dist/repositories/automations.js.map +1 -1
  12. package/dist/repositories/chats.d.ts +55 -0
  13. package/dist/repositories/chats.js +469 -0
  14. package/dist/repositories/chats.js.map +1 -0
  15. package/dist/repositories/content.d.ts +24 -1
  16. package/dist/repositories/content.js +316 -9
  17. package/dist/repositories/content.js.map +1 -1
  18. package/dist/repositories/devices.d.ts +17 -0
  19. package/dist/repositories/devices.js +75 -0
  20. package/dist/repositories/devices.js.map +1 -0
  21. package/dist/repositories/digest-runs.d.ts +14 -0
  22. package/dist/repositories/digest-runs.js +39 -0
  23. package/dist/repositories/digest-runs.js.map +1 -0
  24. package/dist/repositories/discussion.d.ts +16 -4
  25. package/dist/repositories/discussion.js +78 -18
  26. package/dist/repositories/discussion.js.map +1 -1
  27. package/dist/repositories/entitlements.d.ts +31 -9
  28. package/dist/repositories/entitlements.js +213 -17
  29. package/dist/repositories/entitlements.js.map +1 -1
  30. package/dist/repositories/grant-covers.d.ts +20 -0
  31. package/dist/repositories/grant-covers.js +55 -0
  32. package/dist/repositories/grant-covers.js.map +1 -0
  33. package/dist/repositories/identity.d.ts +4 -2
  34. package/dist/repositories/identity.js +32 -1
  35. package/dist/repositories/identity.js.map +1 -1
  36. package/dist/repositories/inserted-row.d.ts +5 -0
  37. package/dist/repositories/inserted-row.js +7 -0
  38. package/dist/repositories/inserted-row.js.map +1 -0
  39. package/dist/repositories/member.d.ts +16 -0
  40. package/dist/repositories/member.js +63 -0
  41. package/dist/repositories/member.js.map +1 -0
  42. package/dist/repositories/members.js +2 -2
  43. package/dist/repositories/members.js.map +1 -1
  44. package/dist/repositories/mentions.d.ts +17 -0
  45. package/dist/repositories/mentions.js +55 -0
  46. package/dist/repositories/mentions.js.map +1 -0
  47. package/dist/repositories/notification-settings.d.ts +19 -0
  48. package/dist/repositories/notification-settings.js +67 -0
  49. package/dist/repositories/notification-settings.js.map +1 -0
  50. package/dist/repositories/notifications.d.ts +30 -0
  51. package/dist/repositories/notifications.js +178 -0
  52. package/dist/repositories/notifications.js.map +1 -0
  53. package/dist/repositories/org-user-profile.d.ts +214 -1
  54. package/dist/repositories/org-user-profile.js +23 -2
  55. package/dist/repositories/org-user-profile.js.map +1 -1
  56. package/dist/repositories/organizations.d.ts +4 -2
  57. package/dist/repositories/organizations.js +23 -8
  58. package/dist/repositories/organizations.js.map +1 -1
  59. package/dist/repositories/pg-errors.js +3 -1
  60. package/dist/repositories/pg-errors.js.map +1 -1
  61. package/dist/repositories/posts.d.ts +35 -0
  62. package/dist/repositories/posts.js +252 -0
  63. package/dist/repositories/posts.js.map +1 -0
  64. package/dist/repositories/subscriptions.d.ts +18 -0
  65. package/dist/repositories/subscriptions.js +72 -0
  66. package/dist/repositories/subscriptions.js.map +1 -0
  67. package/dist/schema/assets.d.ts +19 -2
  68. package/dist/schema/assets.js +10 -4
  69. package/dist/schema/assets.js.map +1 -1
  70. package/dist/schema/automations.d.ts +545 -2
  71. package/dist/schema/automations.js +73 -3
  72. package/dist/schema/automations.js.map +1 -1
  73. package/dist/schema/better-auth.d.ts +1613 -238
  74. package/dist/schema/better-auth.js +107 -22
  75. package/dist/schema/better-auth.js.map +1 -1
  76. package/dist/schema/content.d.ts +642 -5
  77. package/dist/schema/content.js +119 -10
  78. package/dist/schema/content.js.map +1 -1
  79. package/dist/schema/discussion.d.ts +2182 -42
  80. package/dist/schema/discussion.js +461 -11
  81. package/dist/schema/discussion.js.map +1 -1
  82. package/dist/schema/entitlements.d.ts +432 -1
  83. package/dist/schema/entitlements.js +109 -4
  84. package/dist/schema/entitlements.js.map +1 -1
  85. package/dist/schema/identity.d.ts +517 -1
  86. package/dist/schema/identity.js +76 -2
  87. package/dist/schema/identity.js.map +1 -1
  88. package/dist/schema/index.d.ts +7 -7
  89. package/dist/schema/index.js +35 -8
  90. package/dist/schema/organizations.d.ts +4 -4
  91. package/dist/schema/organizations.js +2 -0
  92. package/dist/schema/organizations.js.map +1 -1
  93. package/drizzle/0003_community.sql +340 -0
  94. package/drizzle/0004_assets_sources.sql +1 -0
  95. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  96. package/drizzle/0006_community_review_fixes.sql +24 -0
  97. package/drizzle/0007_notifications.sql +37 -0
  98. package/drizzle/0008_content_access.sql +13 -0
  99. package/drizzle/0009_community_indexes.sql +56 -0
  100. package/drizzle/0010_mcp_access.sql +114 -0
  101. package/drizzle/0011_digest_runs.sql +8 -0
  102. package/drizzle/0012_subscriptions.sql +16 -0
  103. package/drizzle/0013_mentions.sql +10 -0
  104. package/drizzle/0014_notification_settings_devices.sql +36 -0
  105. package/drizzle/0015_automations_drop_source.sql +2 -0
  106. package/drizzle/0016_notifications_automations.sql +48 -0
  107. package/drizzle/meta/0003_snapshot.json +7061 -0
  108. package/drizzle/meta/0004_snapshot.json +7068 -0
  109. package/drizzle/meta/0005_snapshot.json +7180 -0
  110. package/drizzle/meta/0006_snapshot.json +7298 -0
  111. package/drizzle/meta/0007_snapshot.json +7620 -0
  112. package/drizzle/meta/0008_snapshot.json +7666 -0
  113. package/drizzle/meta/0009_snapshot.json +8185 -0
  114. package/drizzle/meta/0010_snapshot.json +8813 -0
  115. package/drizzle/meta/0011_snapshot.json +8868 -0
  116. package/drizzle/meta/0012_snapshot.json +8873 -0
  117. package/drizzle/meta/0013_snapshot.json +8943 -0
  118. package/drizzle/meta/0014_snapshot.json +9305 -0
  119. package/drizzle/meta/0015_snapshot.json +9270 -0
  120. package/drizzle/meta/0016_snapshot.json +9230 -0
  121. package/drizzle/meta/_journal.json +99 -1
  122. package/package.json +2 -2
  123. package/dist/repositories/course-analytics.d.ts +0 -15
  124. package/dist/repositories/course-analytics.js +0 -137
  125. package/dist/repositories/course-analytics.js.map +0 -1
  126. package/dist/repositories/dashboard.d.ts +0 -14
  127. package/dist/repositories/dashboard.js +0 -57
  128. package/dist/repositories/dashboard.js.map +0 -1
  129. package/dist/repositories/learn.d.ts +0 -19
  130. package/dist/repositories/learn.js +0 -56
  131. package/dist/repositories/learn.js.map +0 -1
  132. package/dist/repositories/students.d.ts +0 -16
  133. package/dist/repositories/students.js +0 -160
  134. package/dist/repositories/students.js.map +0 -1
@@ -0,0 +1,67 @@
1
+ import { notificationSettings } from "../schema/identity.js";
2
+ import { orgUsers } from "../schema/organizations.js";
3
+ import "../schema/index.js";
4
+ import { translateDbErrors } from "./pg-errors.js";
5
+ import { and, eq, inArray, sql } from "drizzle-orm";
6
+ import { genId } from "@headless-lms/core/shared/id";
7
+ import { noopLogger } from "@headless-lms/core/shared/logger";
8
+ //#region src/repositories/notification-settings.ts
9
+ const ns = notificationSettings;
10
+ const communityKey = sql`coalesce(${ns.communityId}, '')`;
11
+ const spaceKey = sql`coalesce(${ns.spaceId}, '')`;
12
+ function matchesKey(key) {
13
+ return sql`(${communityKey} = ${key.communityId ?? ""} and ${spaceKey} = ${key.spaceId ?? ""} and ${ns.category} = ${key.category} and ${ns.channel} = ${key.channel})`;
14
+ }
15
+ var DrizzleNotificationSettingsRepository = class {
16
+ db;
17
+ logger;
18
+ constructor(db, logger = noopLogger) {
19
+ this.db = db;
20
+ this.logger = logger;
21
+ }
22
+ async forUsers(orgId, userIds, communityId, categories) {
23
+ if (userIds.length === 0) return [];
24
+ const conditions = [
25
+ eq(ns.orgId, orgId),
26
+ inArray(ns.userId, userIds),
27
+ sql`${communityKey} = ${communityId ?? ""}`
28
+ ];
29
+ if (categories !== null) {
30
+ if (categories.length === 0) return [];
31
+ conditions.push(inArray(ns.category, categories));
32
+ }
33
+ return this.db.select({
34
+ userId: ns.userId,
35
+ communityId: ns.communityId,
36
+ spaceId: ns.spaceId,
37
+ category: ns.category,
38
+ channel: ns.channel,
39
+ enabled: ns.enabled
40
+ }).from(ns).where(and(...conditions));
41
+ }
42
+ async apply(orgId, userId, upserts, deletes, at) {
43
+ if (upserts.length === 0 && deletes.length === 0) return;
44
+ await this.db.transaction(async (tx) => {
45
+ if (deletes.length > 0) await tx.delete(ns).where(and(eq(ns.orgId, orgId), eq(ns.userId, userId), sql`(${sql.join(deletes.map(matchesKey), sql` or `)})`));
46
+ if (upserts.length > 0) {
47
+ const rows = upserts.map((row) => sql`(${orgId}, ${genId("notificationSetting")}, ${userId}, ${row.communityId}, ${row.spaceId}, ${row.category}, ${row.channel}, ${row.enabled}, ${at.toISOString()}::timestamptz)`);
48
+ await tx.execute(sql`insert into notification_settings (org_id, id, user_id, community_id, space_id, category, channel, enabled, updated_at) values ${sql.join(rows, sql`, `)} on conflict (org_id, user_id, coalesce(community_id, ''), coalesce(space_id, ''), category, channel) do update set enabled = excluded.enabled, updated_at = excluded.updated_at`);
49
+ }
50
+ });
51
+ this.logger.debug("notification settings applied", {
52
+ orgId,
53
+ userId,
54
+ upserts: upserts.length,
55
+ deletes: deletes.length
56
+ });
57
+ }
58
+ async switchedOn(orgId, communityId, category, spaceId) {
59
+ const levels = spaceId === null ? [""] : [spaceId, ""];
60
+ return (await this.db.selectDistinct({ orgUserId: orgUsers.id }).from(ns).innerJoin(orgUsers, and(eq(orgUsers.orgId, ns.orgId), eq(orgUsers.userId, ns.userId))).where(and(eq(ns.orgId, orgId), sql`${communityKey} = ${communityId}`, eq(ns.category, category), sql`${spaceKey} in (${sql.join(levels.map((level) => sql`${level}`), sql`, `)})`, sql`${ns.enabled}`))).map((row) => row.orgUserId);
61
+ }
62
+ };
63
+ translateDbErrors(DrizzleNotificationSettingsRepository);
64
+ //#endregion
65
+ export { DrizzleNotificationSettingsRepository };
66
+
67
+ //# sourceMappingURL=notification-settings.js.map
@@ -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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"}
@@ -0,0 +1,30 @@
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
+ //#region src/repositories/notifications.d.ts
5
+ declare class DrizzleNotificationsRepository implements NotificationsRepository {
6
+ private readonly db;
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>;
27
+ }
28
+ //#endregion
29
+ export { DrizzleNotificationsRepository };
30
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1,178 @@
1
+ import { notificationDeliveries, notifications } from "../schema/automations.js";
2
+ import "../schema/index.js";
3
+ import { translateDbErrors } from "./pg-errors.js";
4
+ import { and, arrayOverlaps, desc, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
5
+ //#region src/repositories/notifications.ts
6
+ const n = notifications;
7
+ const d = notificationDeliveries;
8
+ function ref(type, id) {
9
+ return type && id ? {
10
+ type,
11
+ id
12
+ } : null;
13
+ }
14
+ function toStored(row) {
15
+ return {
16
+ id: row.id,
17
+ orgId: row.orgId,
18
+ userId: row.userId,
19
+ type: row.type,
20
+ scope: row.scope,
21
+ reasons: row.reasons,
22
+ matchedReason: row.matchedReason,
23
+ category: row.category,
24
+ communityId: row.communityId,
25
+ spaceId: row.spaceId,
26
+ actorOrgUserId: row.actorOrgUserId,
27
+ subject: ref(row.subjectType, row.subjectId),
28
+ parent: ref(row.parentType, row.parentId),
29
+ display: row.display,
30
+ createdAt: row.createdAt
31
+ };
32
+ }
33
+ function toDelivery(row) {
34
+ return {
35
+ id: row.id,
36
+ notificationId: row.notificationId,
37
+ channel: row.channel,
38
+ status: row.status,
39
+ lastError: row.lastError,
40
+ createdAt: row.createdAt,
41
+ sentAt: row.sentAt
42
+ };
43
+ }
44
+ function listed(orgId, userId, filter) {
45
+ const conditions = [
46
+ eq(n.orgId, orgId),
47
+ eq(n.userId, userId),
48
+ eq(n.inApp, true)
49
+ ];
50
+ if (filter.scope === "org") conditions.push(isNull(n.communityId));
51
+ if (filter.scope === "community") conditions.push(isNotNull(n.communityId));
52
+ if (filter.communityId !== void 0) {
53
+ const community = eq(n.communityId, filter.communityId);
54
+ conditions.push(filter.includeOrg ? or(community, isNull(n.communityId)) : community);
55
+ }
56
+ if (filter.category && filter.category.length > 0) conditions.push(inArray(n.category, filter.category));
57
+ if (filter.reason && filter.reason.length > 0) conditions.push(arrayOverlaps(n.reasons, filter.reason));
58
+ if (filter.type && filter.type.length > 0) conditions.push(inArray(n.type, filter.type));
59
+ if (filter.unread === true) conditions.push(isNull(n.readAt));
60
+ return conditions;
61
+ }
62
+ var DrizzleNotificationsRepository = class {
63
+ db;
64
+ constructor(db) {
65
+ this.db = db;
66
+ }
67
+ async insert(orgId, rows, deliveries) {
68
+ if (rows.length === 0) return;
69
+ await this.db.transaction(async (tx) => {
70
+ const inserted = await tx.insert(n).values(rows.map((row) => ({
71
+ orgId,
72
+ id: row.id,
73
+ userId: row.userId,
74
+ sourceEventId: row.sourceEventId,
75
+ type: row.type,
76
+ scope: row.scope,
77
+ reasons: row.reasons,
78
+ matchedReason: row.matchedReason,
79
+ category: row.category,
80
+ communityId: row.communityId,
81
+ spaceId: row.spaceId,
82
+ actorOrgUserId: row.actorOrgUserId,
83
+ subjectType: row.subject?.type ?? null,
84
+ subjectId: row.subject?.id ?? null,
85
+ parentType: row.parent?.type ?? null,
86
+ parentId: row.parent?.id ?? null,
87
+ display: row.display,
88
+ inApp: row.inApp,
89
+ createdAt: row.createdAt
90
+ }))).onConflictDoNothing().returning({ id: n.id });
91
+ const kept = new Set(inserted.map((row) => row.id));
92
+ const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));
93
+ if (pending.length === 0) return;
94
+ await tx.insert(d).values(pending.map((delivery) => ({
95
+ orgId,
96
+ id: delivery.id,
97
+ notificationId: delivery.notificationId,
98
+ channel: delivery.channel,
99
+ status: "pending"
100
+ }))).onConflictDoNothing();
101
+ });
102
+ }
103
+ async pendingDeliveriesForEvent(orgId, sourceEventId) {
104
+ return this.db.select({ id: d.id }).from(d).innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId))).where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, "pending"))).orderBy(d.id);
105
+ }
106
+ async findDelivery(orgId, deliveryId) {
107
+ const [row] = await this.db.select({
108
+ delivery: d,
109
+ notification: n
110
+ }).from(d).innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId))).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId))).limit(1);
111
+ return row ? {
112
+ delivery: toDelivery(row.delivery),
113
+ notification: toStored(row.notification)
114
+ } : null;
115
+ }
116
+ async markSent(orgId, deliveryId, at) {
117
+ await this.db.update(d).set({
118
+ status: "sent",
119
+ sentAt: at
120
+ }).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
121
+ }
122
+ async recordError(orgId, deliveryId, error) {
123
+ await this.db.update(d).set({ lastError: error }).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
124
+ }
125
+ async markFailed(orgId, deliveryId, error) {
126
+ await this.db.update(d).set({
127
+ status: "failed",
128
+ lastError: error
129
+ }).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
130
+ }
131
+ async page(orgId, userId, filter, page) {
132
+ const where = and(...listed(orgId, userId, filter));
133
+ const [rows, counts] = await Promise.all([this.db.select().from(n).where(where).orderBy(desc(n.createdAt), desc(n.id)).limit(page.pageSize).offset((page.page - 1) * page.pageSize), this.db.select({
134
+ total: sql`count(*)::int`,
135
+ unread: sql`(count(*) filter (where ${n.readAt} is null))::int`
136
+ }).from(n).where(where)]);
137
+ return {
138
+ items: rows.map((row) => ({
139
+ ...toStored(row),
140
+ readAt: row.readAt
141
+ })),
142
+ total: Number(counts[0]?.total ?? 0),
143
+ unreadCount: Number(counts[0]?.unread ?? 0)
144
+ };
145
+ }
146
+ async find(orgId, userId, notificationId) {
147
+ const [row] = await this.db.select().from(n).where(and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true))).limit(1);
148
+ return row ? {
149
+ ...toStored(row),
150
+ readAt: row.readAt
151
+ } : null;
152
+ }
153
+ async unreadCounts(orgId, userId, communityIds) {
154
+ const unread = and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true), isNull(n.readAt));
155
+ const [org, communities] = await Promise.all([this.db.select({ unread: sql`count(*)::int` }).from(n).where(and(unread, isNull(n.communityId))), communityIds.length === 0 ? Promise.resolve([]) : this.db.select({
156
+ communityId: n.communityId,
157
+ unread: sql`count(*)::int`
158
+ }).from(n).where(and(unread, inArray(n.communityId, communityIds))).groupBy(n.communityId)]);
159
+ return {
160
+ org: Number(org[0]?.unread ?? 0),
161
+ communities: communities.flatMap((row) => row.communityId ? [{
162
+ communityId: row.communityId,
163
+ unread: Number(row.unread)
164
+ }] : [])
165
+ };
166
+ }
167
+ async markRead(orgId, userId, notificationId, at) {
168
+ return (await this.db.update(n).set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` }).where(and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true))).returning({ id: n.id })).length > 0;
169
+ }
170
+ async markAllRead(orgId, userId, filter, at) {
171
+ return (await this.db.update(n).set({ readAt: at }).where(and(...listed(orgId, userId, filter), isNull(n.readAt))).returning({ id: n.id })).length;
172
+ }
173
+ };
174
+ translateDbErrors(DrizzleNotificationsRepository);
175
+ //#endregion
176
+ export { DrizzleNotificationsRepository };
177
+
178
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import {\n and,\n arrayOverlaps,\n desc,\n eq,\n inArray,\n isNotNull,\n isNull,\n or,\n sql,\n type SQL,\n} from \"drizzle-orm\";\nimport type {\n ListedNotification,\n NewDelivery,\n NewNotification,\n NotificationPage,\n NotificationsRepository,\n UnreadCounts,\n} from \"@headless-lms/core/automations\";\nimport type {\n Delivery,\n NotificationFilter,\n StoredNotification,\n SubjectRef,\n} from \"@headless-lms/core/types\";\nimport type { DbExecutor } from \"../client.js\";\nimport { notificationDeliveries, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst n = notifications;\nconst d = notificationDeliveries;\n\ntype NotificationRow = typeof notifications.$inferSelect;\ntype DeliveryRow = typeof notificationDeliveries.$inferSelect;\n\nfunction ref(type: string | null, id: string | null): SubjectRef | null {\n return type && id ? { type: type as SubjectRef[\"type\"], id } : null;\n}\n\nfunction toStored(row: NotificationRow): StoredNotification {\n return {\n id: row.id,\n orgId: row.orgId,\n userId: row.userId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subject: ref(row.subjectType, row.subjectId),\n parent: ref(row.parentType, row.parentId),\n display: row.display,\n createdAt: row.createdAt,\n };\n}\n\nfunction toDelivery(row: DeliveryRow): Delivery {\n return {\n id: row.id,\n notificationId: row.notificationId,\n channel: row.channel,\n status: row.status,\n lastError: row.lastError,\n createdAt: row.createdAt,\n sentAt: row.sentAt,\n };\n}\n\nfunction listed(orgId: string, userId: string, filter: NotificationFilter): SQL[] {\n const conditions: SQL[] = [eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true)];\n if (filter.scope === \"org\") {\n conditions.push(isNull(n.communityId));\n }\n if (filter.scope === \"community\") {\n conditions.push(isNotNull(n.communityId));\n }\n if (filter.communityId !== undefined) {\n const community = eq(n.communityId, filter.communityId);\n conditions.push(filter.includeOrg ? or(community, isNull(n.communityId))! : community);\n }\n if (filter.category && filter.category.length > 0) {\n conditions.push(inArray(n.category, filter.category));\n }\n if (filter.reason && filter.reason.length > 0) {\n conditions.push(arrayOverlaps(n.reasons, filter.reason));\n }\n if (filter.type && filter.type.length > 0) {\n conditions.push(inArray(n.type, filter.type));\n }\n if (filter.unread === true) {\n conditions.push(isNull(n.readAt));\n }\n return conditions;\n}\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: DbExecutor) {}\n\n async insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n const inserted = await tx\n .insert(n)\n .values(\n rows.map((row) => ({\n orgId,\n id: row.id,\n userId: row.userId,\n sourceEventId: row.sourceEventId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subjectType: row.subject?.type ?? null,\n subjectId: row.subject?.id ?? null,\n parentType: row.parent?.type ?? null,\n parentId: row.parent?.id ?? null,\n display: row.display,\n inApp: row.inApp,\n createdAt: row.createdAt,\n })),\n )\n .onConflictDoNothing()\n .returning({ id: n.id });\n const kept = new Set(inserted.map((row) => row.id));\n const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));\n if (pending.length === 0) {\n return;\n }\n await tx\n .insert(d)\n .values(\n pending.map((delivery) => ({\n orgId,\n id: delivery.id,\n notificationId: delivery.notificationId,\n channel: delivery.channel,\n status: \"pending\" as const,\n })),\n )\n .onConflictDoNothing();\n });\n }\n\n async pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{ id: string }[]> {\n return this.db\n .select({ id: d.id })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, \"pending\")))\n .orderBy(d.id);\n }\n\n async findDelivery(\n orgId: string,\n deliveryId: string,\n ): Promise<{ delivery: Delivery; notification: StoredNotification } | null> {\n const [row] = await this.db\n .select({ delivery: d, notification: n })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId)))\n .limit(1);\n return row\n ? { delivery: toDelivery(row.delivery), notification: toStored(row.notification) }\n : null;\n }\n\n async markSent(orgId: string, deliveryId: string, at: Date): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"sent\", sentAt: at })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async recordError(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async markFailed(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"failed\", lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async page(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n ): Promise<NotificationPage> {\n const where = and(...listed(orgId, userId, filter));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(n)\n .where(where)\n .orderBy(desc(n.createdAt), desc(n.id))\n .limit(page.pageSize)\n .offset((page.page - 1) * page.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${n.readAt} is null))::int`,\n })\n .from(n)\n .where(where),\n ]);\n return {\n items: rows.map((row) => ({ ...toStored(row), readAt: row.readAt })),\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async find(\n orgId: string,\n userId: string,\n notificationId: string,\n ): Promise<ListedNotification | null> {\n const [row] = await this.db\n .select()\n .from(n)\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .limit(1);\n return row ? { ...toStored(row), readAt: row.readAt } : null;\n }\n\n async unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts> {\n const unread = and(\n eq(n.orgId, orgId),\n eq(n.userId, userId),\n eq(n.inApp, true),\n isNull(n.readAt),\n );\n const [org, communities] = await Promise.all([\n this.db\n .select({ unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, isNull(n.communityId))),\n communityIds.length === 0\n ? Promise.resolve([])\n : this.db\n .select({ communityId: n.communityId, unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, inArray(n.communityId, communityIds)))\n .groupBy(n.communityId),\n ]);\n return {\n org: Number(org[0]?.unread ?? 0),\n communities: communities.flatMap((row) =>\n row.communityId ? [{ communityId: row.communityId, unread: Number(row.unread) }] : [],\n ),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<boolean> {\n const rows = await this.db\n .update(n)\n .set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .returning({ id: n.id });\n return rows.length > 0;\n }\n\n async markAllRead(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n at: Date,\n ): Promise<number> {\n const rows = await this.db\n .update(n)\n .set({ readAt: at })\n .where(and(...listed(orgId, userId, filter), isNull(n.readAt)))\n .returning({ id: n.id });\n return rows.length;\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":"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"}
@@ -87,6 +87,219 @@ declare const orgUserProfileColumns: {
87
87
  generated: undefined;
88
88
  }, {}, {}>;
89
89
  };
90
+ declare const authorProfileColumns: {
91
+ role: import("drizzle-orm/pg-core").PgColumn<{
92
+ name: "role";
93
+ tableName: "org_users";
94
+ dataType: "string";
95
+ columnType: "PgText";
96
+ data: "student" | "owner" | "admin" | "instructor" | "moderator";
97
+ driverParam: string;
98
+ notNull: true;
99
+ hasDefault: false;
100
+ isPrimaryKey: false;
101
+ isAutoincrement: false;
102
+ hasRuntimeDefault: false;
103
+ enumValues: ["owner", "admin", "instructor", "moderator", "student"];
104
+ baseColumn: never;
105
+ identity: undefined;
106
+ generated: undefined;
107
+ }, {}, {}>;
108
+ displayName: import("drizzle-orm/pg-core").PgColumn<{
109
+ name: "display_name";
110
+ tableName: "user_profiles";
111
+ dataType: "string";
112
+ columnType: "PgText";
113
+ data: string;
114
+ driverParam: string;
115
+ notNull: false;
116
+ hasDefault: false;
117
+ isPrimaryKey: false;
118
+ isAutoincrement: false;
119
+ hasRuntimeDefault: false;
120
+ enumValues: [string, ...string[]];
121
+ baseColumn: never;
122
+ identity: undefined;
123
+ generated: undefined;
124
+ }, {}, {}>;
125
+ headline: import("drizzle-orm/pg-core").PgColumn<{
126
+ name: "headline";
127
+ tableName: "user_profiles";
128
+ dataType: "string";
129
+ columnType: "PgText";
130
+ data: string;
131
+ driverParam: string;
132
+ notNull: false;
133
+ hasDefault: false;
134
+ isPrimaryKey: false;
135
+ isAutoincrement: false;
136
+ hasRuntimeDefault: false;
137
+ enumValues: [string, ...string[]];
138
+ baseColumn: never;
139
+ identity: undefined;
140
+ generated: undefined;
141
+ }, {}, {}>;
142
+ imageAssetOrgId: import("drizzle-orm/pg-core").PgColumn<{
143
+ name: "image_asset_org_id";
144
+ tableName: "user_profiles";
145
+ dataType: "string";
146
+ columnType: "PgText";
147
+ data: string;
148
+ driverParam: string;
149
+ notNull: false;
150
+ hasDefault: false;
151
+ isPrimaryKey: false;
152
+ isAutoincrement: false;
153
+ hasRuntimeDefault: false;
154
+ enumValues: [string, ...string[]];
155
+ baseColumn: never;
156
+ identity: undefined;
157
+ generated: undefined;
158
+ }, {}, {}>;
159
+ imageAssetId: import("drizzle-orm/pg-core").PgColumn<{
160
+ name: "image_asset_id";
161
+ tableName: "user_profiles";
162
+ dataType: "string";
163
+ columnType: "PgText";
164
+ data: string;
165
+ driverParam: string;
166
+ notNull: false;
167
+ hasDefault: false;
168
+ isPrimaryKey: false;
169
+ isAutoincrement: false;
170
+ hasRuntimeDefault: false;
171
+ enumValues: [string, ...string[]];
172
+ baseColumn: never;
173
+ identity: undefined;
174
+ generated: undefined;
175
+ }, {}, {}>;
176
+ id: import("drizzle-orm/pg-core").PgColumn<{
177
+ name: "id";
178
+ tableName: "org_users";
179
+ dataType: "string";
180
+ columnType: "PgText";
181
+ data: string;
182
+ driverParam: string;
183
+ notNull: true;
184
+ hasDefault: true;
185
+ isPrimaryKey: false;
186
+ isAutoincrement: false;
187
+ hasRuntimeDefault: true;
188
+ enumValues: [string, ...string[]];
189
+ baseColumn: never;
190
+ identity: undefined;
191
+ generated: undefined;
192
+ }, {}, {}>;
193
+ firstName: import("drizzle-orm/pg-core").PgColumn<{
194
+ name: "first_name";
195
+ tableName: "users";
196
+ dataType: "string";
197
+ columnType: "PgText";
198
+ data: string;
199
+ driverParam: string;
200
+ notNull: false;
201
+ hasDefault: false;
202
+ isPrimaryKey: false;
203
+ isAutoincrement: false;
204
+ hasRuntimeDefault: false;
205
+ enumValues: [string, ...string[]];
206
+ baseColumn: never;
207
+ identity: undefined;
208
+ generated: undefined;
209
+ }, {}, {}>;
210
+ lastName: import("drizzle-orm/pg-core").PgColumn<{
211
+ name: "last_name";
212
+ tableName: "users";
213
+ dataType: "string";
214
+ columnType: "PgText";
215
+ data: string;
216
+ driverParam: string;
217
+ notNull: false;
218
+ hasDefault: false;
219
+ isPrimaryKey: false;
220
+ isAutoincrement: false;
221
+ hasRuntimeDefault: false;
222
+ enumValues: [string, ...string[]];
223
+ baseColumn: never;
224
+ identity: undefined;
225
+ generated: undefined;
226
+ }, {}, {}>;
227
+ email: import("drizzle-orm/pg-core").PgColumn<{
228
+ name: "email";
229
+ tableName: "users";
230
+ dataType: "string";
231
+ columnType: "PgText";
232
+ data: string;
233
+ driverParam: string;
234
+ notNull: true;
235
+ hasDefault: false;
236
+ isPrimaryKey: false;
237
+ isAutoincrement: false;
238
+ hasRuntimeDefault: false;
239
+ enumValues: [string, ...string[]];
240
+ baseColumn: never;
241
+ identity: undefined;
242
+ generated: undefined;
243
+ }, {}, {}>;
244
+ image: import("drizzle-orm/pg-core").PgColumn<{
245
+ name: "image";
246
+ tableName: "ba_user";
247
+ dataType: "string";
248
+ columnType: "PgText";
249
+ data: string;
250
+ driverParam: string;
251
+ notNull: false;
252
+ hasDefault: false;
253
+ isPrimaryKey: false;
254
+ isAutoincrement: false;
255
+ hasRuntimeDefault: false;
256
+ enumValues: [string, ...string[]];
257
+ baseColumn: never;
258
+ identity: undefined;
259
+ generated: undefined;
260
+ }, {}, {}>;
261
+ };
262
+ declare const studentFirstName: import("drizzle-orm").SQL<string | null>;
263
+ declare const studentLastName: import("drizzle-orm").SQL<string | null>;
264
+ declare const studentProfileColumns: {
265
+ id: import("drizzle-orm/pg-core").PgColumn<{
266
+ name: "id";
267
+ tableName: "org_users";
268
+ dataType: "string";
269
+ columnType: "PgText";
270
+ data: string;
271
+ driverParam: string;
272
+ notNull: true;
273
+ hasDefault: true;
274
+ isPrimaryKey: false;
275
+ isAutoincrement: false;
276
+ hasRuntimeDefault: true;
277
+ enumValues: [string, ...string[]];
278
+ baseColumn: never;
279
+ identity: undefined;
280
+ generated: undefined;
281
+ }, {}, {}>;
282
+ firstName: import("drizzle-orm").SQL<string | null>;
283
+ lastName: import("drizzle-orm").SQL<string | null>;
284
+ email: import("drizzle-orm/pg-core").PgColumn<{
285
+ name: "email";
286
+ tableName: "users";
287
+ dataType: "string";
288
+ columnType: "PgText";
289
+ data: string;
290
+ driverParam: string;
291
+ notNull: true;
292
+ hasDefault: false;
293
+ isPrimaryKey: false;
294
+ isAutoincrement: false;
295
+ hasRuntimeDefault: false;
296
+ enumValues: [string, ...string[]];
297
+ baseColumn: never;
298
+ identity: undefined;
299
+ generated: undefined;
300
+ }, {}, {}>;
301
+ image: import("drizzle-orm").SQL<string | null>;
302
+ };
90
303
  //#endregion
91
- export { orgUserProfileColumns };
304
+ export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
92
305
  //# sourceMappingURL=org-user-profile.d.ts.map
@@ -1,6 +1,7 @@
1
- import { users } from "../schema/identity.js";
1
+ import { userProfiles, users } from "../schema/identity.js";
2
2
  import { orgUsers } from "../schema/organizations.js";
3
3
  import { user } from "../schema/better-auth.js";
4
+ import { sql } from "drizzle-orm";
4
5
  //#region src/repositories/org-user-profile.ts
5
6
  /** Spread into a `.select({ ... })` to get the profile columns. */
6
7
  const orgUserProfileColumns = {
@@ -10,7 +11,27 @@ const orgUserProfileColumns = {
10
11
  email: users.email,
11
12
  image: user.image
12
13
  };
14
+ const authorProfileColumns = {
15
+ ...orgUserProfileColumns,
16
+ role: orgUsers.role,
17
+ displayName: userProfiles.displayName,
18
+ headline: userProfiles.headline,
19
+ imageAssetOrgId: userProfiles.imageAssetOrgId,
20
+ imageAssetId: userProfiles.imageAssetId
21
+ };
22
+ const profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (
23
+ select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}
24
+ )))`;
25
+ const studentFirstName = sql`case when ${profileVisible} then ${users.firstName} end`;
26
+ const studentLastName = sql`case when ${profileVisible} then ${users.lastName} end`;
27
+ const studentProfileColumns = {
28
+ id: orgUsers.id,
29
+ firstName: studentFirstName,
30
+ lastName: studentLastName,
31
+ email: users.email,
32
+ image: sql`case when ${profileVisible} then ${user.image} end`
33
+ };
13
34
  //#endregion
14
- export { orgUserProfileColumns };
35
+ export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
15
36
 
16
37
  //# sourceMappingURL=org-user-profile.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\n\n/** Spread into a `.select({ ... })` to get the profile columns. */\nexport const orgUserProfileColumns = {\n id: orgUsers.id,\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n image: user.image,\n};\n"],"mappings":";;;;;AAmBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd"}
1
+ {"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { sql } from \"drizzle-orm\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\n\n/** Spread into a `.select({ ... })` to get the profile columns. */\nexport const orgUserProfileColumns = {\n id: orgUsers.id,\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n image: user.image,\n};\n\nexport const authorProfileColumns = {\n ...orgUserProfileColumns,\n role: orgUsers.role,\n displayName: userProfiles.displayName,\n headline: userProfiles.headline,\n imageAssetOrgId: userProfiles.imageAssetOrgId,\n imageAssetId: userProfiles.imageAssetId,\n};\n\n// A person is shared across organizations. Until an invited student accepts,\n// the inviting org sees the name and avatar only when the profile is its own:\n// no account yet and no other organization.\nconst profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (\n select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}\n)))`;\n\nexport const studentFirstName = sql<\n string | null\n>`case when ${profileVisible} then ${users.firstName} end`;\nexport const studentLastName = sql<\n string | null\n>`case when ${profileVisible} then ${users.lastName} end`;\n\nexport const studentProfileColumns = {\n id: orgUsers.id,\n firstName: studentFirstName,\n lastName: studentLastName,\n email: users.email,\n image: sql<string | null>`case when ${profileVisible} then ${user.image} end`,\n};\n"],"mappings":";;;;;;AAoBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd;AAEA,MAAa,uBAAuB;CAClC,GAAG;CACH,MAAM,SAAS;CACf,aAAa,aAAa;CAC1B,UAAU,aAAa;CACvB,iBAAiB,aAAa;CAC9B,cAAc,aAAa;AAC7B;AAKA,MAAM,iBAAiB,GAAG,IAAI,SAAS,OAAO,oBAAoB,MAAM,WAAW;wDAC3B,MAAM,GAAG,uBAAuB,SAAS,MAAM;;AAGvG,MAAa,mBAAmB,GAE/B,aAAa,eAAe,QAAQ,MAAM,UAAU;AACrD,MAAa,kBAAkB,GAE9B,aAAa,eAAe,QAAQ,MAAM,SAAS;AAEpD,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW;CACX,UAAU;CACV,OAAO,MAAM;CACb,OAAO,GAAkB,aAAa,eAAe,QAAQ,KAAK,MAAM;AAC1E"}
@@ -1,5 +1,5 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
2
+ import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, Role, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
3
3
  import { Logger } from "@headless-lms/core/shared/ports";
4
4
  //#region src/repositories/organizations.d.ts
5
5
  declare class DrizzleOrganizationsRepository implements OrganizationsRepository {
@@ -10,6 +10,7 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
10
10
  update(id: string, input: UpdateOrganizationInput): Promise<Organization | null>;
11
11
  delete(id: string): Promise<Organization>;
12
12
  findById(id: string): Promise<Organization | null>;
13
+ listIds(): Promise<string[]>;
13
14
  findByExternalId(externalId: string): Promise<Organization | null>;
14
15
  findBySlug(slug: string): Promise<Organization | null>;
15
16
  upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite>;
@@ -20,10 +21,11 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
20
21
  findOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
21
22
  findOrgUsersByUser(userId: string): Promise<OrgUser[]>;
22
23
  findOrgUserById(orgId: string, id: string): Promise<OrgUser | null>;
24
+ findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]>;
25
+ findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]>;
23
26
  createOrgUser(input: CreateOrgUserInput): Promise<OrgUser>;
24
27
  updateOrgUser(orgId: string, id: string, patch: UpdateOrgUserInput): Promise<OrgUser | null>;
25
28
  findStudentOrgUsers(userId: string): Promise<OrgUser[]>;
26
- cancelPendingInvite(orgId: string, email: string): Promise<Invite | null>;
27
29
  deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null>;
28
30
  }
29
31
  //#endregion