@headless-lms/adapter-db 0.4.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 (90) 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 +5 -4
  5. package/dist/index.js +4 -3
  6. package/dist/repositories/admin.d.ts +12 -4
  7. package/dist/repositories/admin.js +164 -22
  8. package/dist/repositories/admin.js.map +1 -1
  9. package/dist/repositories/assets.d.ts +2 -1
  10. package/dist/repositories/assets.js +11 -1
  11. package/dist/repositories/assets.js.map +1 -1
  12. package/dist/repositories/automations.js +1 -1
  13. package/dist/repositories/chats.d.ts +13 -4
  14. package/dist/repositories/chats.js +164 -70
  15. package/dist/repositories/chats.js.map +1 -1
  16. package/dist/repositories/content.d.ts +22 -5
  17. package/dist/repositories/content.js +198 -40
  18. package/dist/repositories/content.js.map +1 -1
  19. package/dist/repositories/credentials.js +1 -1
  20. package/dist/repositories/devices.js +1 -1
  21. package/dist/repositories/digest-runs.js +1 -1
  22. package/dist/repositories/discussion.d.ts +1 -1
  23. package/dist/repositories/discussion.js +9 -3
  24. package/dist/repositories/discussion.js.map +1 -1
  25. package/dist/repositories/entitlements.d.ts +10 -10
  26. package/dist/repositories/entitlements.js +47 -56
  27. package/dist/repositories/entitlements.js.map +1 -1
  28. package/dist/repositories/event-reports.d.ts +16 -0
  29. package/dist/repositories/event-reports.js +199 -0
  30. package/dist/repositories/event-reports.js.map +1 -0
  31. package/dist/repositories/grant-covers.d.ts +4 -3
  32. package/dist/repositories/grant-covers.js +7 -4
  33. package/dist/repositories/grant-covers.js.map +1 -1
  34. package/dist/repositories/identity.js +1 -1
  35. package/dist/repositories/integrations.js +1 -1
  36. package/dist/repositories/member.d.ts +12 -4
  37. package/dist/repositories/member.js +19 -7
  38. package/dist/repositories/member.js.map +1 -1
  39. package/dist/repositories/members.d.ts +2 -5
  40. package/dist/repositories/members.js +39 -94
  41. package/dist/repositories/members.js.map +1 -1
  42. package/dist/repositories/mentions.js +1 -1
  43. package/dist/repositories/notification-settings.js +1 -1
  44. package/dist/repositories/notifications.js +2 -2
  45. package/dist/repositories/notifications.js.map +1 -1
  46. package/dist/repositories/org-user-profile.d.ts +0 -17
  47. package/dist/repositories/org-user-profile.js +0 -1
  48. package/dist/repositories/org-user-profile.js.map +1 -1
  49. package/dist/repositories/organizations.d.ts +3 -4
  50. package/dist/repositories/organizations.js +21 -46
  51. package/dist/repositories/organizations.js.map +1 -1
  52. package/dist/repositories/outbox.d.ts +5 -1
  53. package/dist/repositories/outbox.js +12 -3
  54. package/dist/repositories/outbox.js.map +1 -1
  55. package/dist/repositories/posts.js +1 -1
  56. package/dist/repositories/progress.js +1 -1
  57. package/dist/repositories/roles.d.ts +35 -0
  58. package/dist/repositories/roles.js +244 -0
  59. package/dist/repositories/roles.js.map +1 -0
  60. package/dist/repositories/settings.js +1 -1
  61. package/dist/repositories/subscriptions.js +1 -1
  62. package/dist/schema/assets.d.ts +17 -0
  63. package/dist/schema/assets.js +1 -0
  64. package/dist/schema/assets.js.map +1 -1
  65. package/dist/schema/automations.d.ts +2 -2
  66. package/dist/schema/automations.js +1 -1
  67. package/dist/schema/automations.js.map +1 -1
  68. package/dist/schema/content.d.ts +437 -3
  69. package/dist/schema/content.js +70 -3
  70. package/dist/schema/content.js.map +1 -1
  71. package/dist/schema/discussion.d.ts +18 -1
  72. package/dist/schema/discussion.js +4 -2
  73. package/dist/schema/discussion.js.map +1 -1
  74. package/dist/schema/index.d.ts +4 -3
  75. package/dist/schema/index.js +7 -2
  76. package/dist/schema/index.js.map +1 -1
  77. package/dist/schema/organizations.d.ts +5 -22
  78. package/dist/schema/organizations.js +1 -13
  79. package/dist/schema/organizations.js.map +1 -1
  80. package/dist/schema/roles.d.ts +369 -0
  81. package/dist/schema/roles.js +71 -0
  82. package/dist/schema/roles.js.map +1 -0
  83. package/drizzle/0017_configurable_roles.sql +38 -0
  84. package/drizzle/0018_messages_client_id.sql +2 -0
  85. package/drizzle/0019_events.sql +44 -0
  86. package/drizzle/meta/0017_snapshot.json +9518 -0
  87. package/drizzle/meta/0018_snapshot.json +9557 -0
  88. package/drizzle/meta/0019_snapshot.json +9919 -0
  89. package/drizzle/meta/_journal.json +21 -0
  90. package/package.json +2 -2
@@ -0,0 +1,199 @@
1
+ import { SETTINGS_NAMESPACE } from "./content.js";
2
+ import { oneOf } from "./grant-covers.js";
3
+ import { sql } from "drizzle-orm";
4
+ import { eventSettingsDefaults } from "@headless-lms/core/schemas";
5
+ //#region src/repositories/event-reports.ts
6
+ function attendance(orgId, eventIds) {
7
+ return sql`
8
+ select ca.content_id, ca.org_user_id,
9
+ max(case when e.content_id is not null then e.source end) as direct_source,
10
+ min(case when e.content_id is not null then ca.granted_at end) as direct_at,
11
+ min(ca.granted_at) as any_at
12
+ from content_access ca
13
+ join entitlements e on e.org_id = ca.org_id and e.id = ca.entitlement_id
14
+ where ca.org_id = ${orgId} and ${oneOf(sql`ca.content_id`, eventIds)}
15
+ group by ca.content_id, ca.org_user_id
16
+ `;
17
+ }
18
+ const DISPLAY_NAME = sql`coalesce(
19
+ nullif(trim(up.display_name), ''),
20
+ nullif(trim(concat_ws(' ', u.first_name, u.last_name)), '')
21
+ )`;
22
+ const SOURCE = sql`case
23
+ when a.direct_source in ('self', 'automation') then a.direct_source
24
+ when a.direct_source is not null then 'manual'
25
+ else 'bundle'
26
+ end`;
27
+ async function eventAttendeePage(db, orgId, eventId, page) {
28
+ const rows = (await db.execute(sql`
29
+ with a as (${attendance(orgId, [eventId])})
30
+ select a.org_user_id,
31
+ ${DISPLAY_NAME} as display_name,
32
+ case when up.image_asset_org_id = ${orgId} then up.image_asset_id end as image_asset_id,
33
+ ${SOURCE} as source,
34
+ coalesce(a.direct_at, a.any_at) as granted_at,
35
+ count(*) over () as total
36
+ from a
37
+ join org_users ou on ou.org_id = ${orgId} and ou.id = a.org_user_id
38
+ left join users u on u.id = ou.user_id
39
+ left join user_profiles up on up.user_id = ou.user_id
40
+ order by coalesce(a.direct_at, a.any_at), a.org_user_id
41
+ limit ${page.pageSize}
42
+ offset ${(page.page - 1) * page.pageSize}
43
+ `)).rows;
44
+ const total = rows.length > 0 || page.page === 1 ? Number(rows[0]?.total ?? 0) : await eventAttendeeCount(db, orgId, eventId);
45
+ return {
46
+ rows: rows.map((row) => ({
47
+ orgUserId: row.org_user_id,
48
+ displayName: row.display_name,
49
+ imageAssetId: row.image_asset_id,
50
+ source: row.source,
51
+ grantedAt: new Date(row.granted_at)
52
+ })),
53
+ total,
54
+ page: page.page,
55
+ pageSize: page.pageSize
56
+ };
57
+ }
58
+ async function eventAttendeeCount(db, orgId, eventId) {
59
+ const result = await db.execute(sql`
60
+ select count(distinct ca.org_user_id) as total
61
+ from content_access ca
62
+ where ca.org_id = ${orgId} and ca.content_id = ${eventId}
63
+ `);
64
+ return Number(result.rows[0]?.total ?? 0);
65
+ }
66
+ async function eventAnalyticsFigures(db, orgId, eventId, startsAt) {
67
+ const [sources, revoked, overTime] = await Promise.all([
68
+ db.execute(sql`
69
+ with a as (${attendance(orgId, [eventId])})
70
+ select ${SOURCE} as source, count(*) as count from a group by 1
71
+ `),
72
+ db.execute(sql`
73
+ select count(*) as count from entitlements e
74
+ where e.org_id = ${orgId} and e.content_id = ${eventId} and e.status = 'revoked'
75
+ `),
76
+ db.execute(sql`
77
+ select to_char((e.granted_at at time zone 'utc')::date, 'YYYY-MM-DD') as date,
78
+ count(*) as count
79
+ from entitlements e
80
+ where e.org_id = ${orgId}
81
+ and e.content_id = ${eventId}
82
+ and e.status = 'active'
83
+ and (e.expires_at is null or e.expires_at > now())
84
+ and e.granted_at < ${startsAt}
85
+ group by 1
86
+ order by 1
87
+ `)
88
+ ]);
89
+ const bySource = {
90
+ self: 0,
91
+ manual: 0,
92
+ automation: 0,
93
+ bundle: 0
94
+ };
95
+ for (const row of sources.rows) bySource[row.source] = Number(row.count);
96
+ return {
97
+ attendees: bySource.self + bySource.manual + bySource.automation + bySource.bundle,
98
+ bySource,
99
+ revoked: Number(revoked.rows[0]?.count ?? 0),
100
+ grantsOverTime: overTime.rows.map((row) => ({
101
+ date: row.date,
102
+ count: Number(row.count)
103
+ }))
104
+ };
105
+ }
106
+ async function eventFacesFor(db, orgId, eventIds, limit) {
107
+ const faces = Object.fromEntries(eventIds.map((id) => [id, {
108
+ total: 0,
109
+ rows: []
110
+ }]));
111
+ if (eventIds.length === 0) return faces;
112
+ const result = await db.execute(sql`
113
+ with a as (${attendance(orgId, eventIds)}),
114
+ ranked as (
115
+ select a.content_id, a.org_user_id,
116
+ ${DISPLAY_NAME} as display_name,
117
+ case when up.image_asset_org_id = ${orgId} then up.image_asset_id end as image_asset_id,
118
+ row_number() over (
119
+ partition by a.content_id
120
+ order by coalesce(a.direct_at, a.any_at), a.org_user_id
121
+ ) as position,
122
+ count(*) over (partition by a.content_id) as total
123
+ from a
124
+ join org_users ou on ou.org_id = ${orgId} and ou.id = a.org_user_id
125
+ left join users u on u.id = ou.user_id
126
+ left join user_profiles up on up.user_id = ou.user_id
127
+ )
128
+ select content_id, org_user_id, display_name, image_asset_id, total
129
+ from ranked
130
+ where position <= ${limit}
131
+ order by content_id, position
132
+ `);
133
+ for (const row of result.rows) {
134
+ const entry = faces[row.content_id];
135
+ if (!entry) continue;
136
+ entry.total = Number(row.total);
137
+ entry.rows.push({
138
+ orgUserId: row.org_user_id,
139
+ displayName: row.display_name,
140
+ imageAssetId: row.image_asset_id
141
+ });
142
+ }
143
+ return faces;
144
+ }
145
+ async function directEventGrantIds(db, orgId, orgUserId, eventIds) {
146
+ return (await db.execute(sql`
147
+ select distinct e.content_id from entitlements e
148
+ where e.org_id = ${orgId}
149
+ and e.org_user_id = ${orgUserId}
150
+ and ${oneOf(sql`e.content_id`, eventIds)}
151
+ and e.status = 'active'
152
+ and (e.expires_at is null or e.expires_at > now())
153
+ `)).rows.map((row) => row.content_id);
154
+ }
155
+ async function memberEventIds(db, orgId, query, reader, now) {
156
+ const attends = sql`exists (
157
+ select 1 from content_access ca
158
+ where ca.org_id = ev.org_id and ca.content_id = ev.id and ca.org_user_id = ${reader.orgUserId}
159
+ )`;
160
+ const sees = reader.readAll ? sql`true` : sql`(
161
+ ${oneOf(sql`ev.id`, reader.readIds)}
162
+ or (ev.status = 'published' and coalesce(s.value ->> 'type', ${eventSettingsDefaults.type}) = 'public')
163
+ or ${attends}
164
+ )`;
165
+ const past = query.when === "past";
166
+ const where = sql`
167
+ ev.org_id = ${orgId}
168
+ and ev.status in ('published', 'canceled')
169
+ and ${sees}
170
+ and ${query.attending ? attends : sql`true`}
171
+ and ${past ? sql`ev.ends_at <= ${now}` : sql`ev.ends_at > ${now}`}
172
+ `;
173
+ const from = sql`
174
+ from events ev
175
+ left join settings s on s.org_id = ev.org_id and s.namespace = ${SETTINGS_NAMESPACE} and s.scope_id = ev.id
176
+ `;
177
+ const order = past ? sql`ev.starts_at desc, ev.id desc` : sql`ev.starts_at asc, ev.id asc`;
178
+ const [rows, totals] = await Promise.all([db.execute(sql`
179
+ select ev.id ${from} where ${where} order by ${order}
180
+ limit ${query.pageSize} offset ${(query.page - 1) * query.pageSize}
181
+ `), db.execute(sql`select count(*) as total ${from} where ${where}`)]);
182
+ return {
183
+ rows: rows.rows.map((row) => row.id),
184
+ total: Number(totals.rows[0]?.total ?? 0),
185
+ page: query.page,
186
+ pageSize: query.pageSize
187
+ };
188
+ }
189
+ async function upcomingEventCount(db, orgId) {
190
+ const result = await db.execute(sql`
191
+ select count(*) as total from events ev
192
+ where ev.org_id = ${orgId} and ev.status = 'published' and ev.ends_at > now()
193
+ `);
194
+ return Number(result.rows[0]?.total ?? 0);
195
+ }
196
+ //#endregion
197
+ export { directEventGrantIds, eventAnalyticsFigures, eventAttendeePage, eventFacesFor, memberEventIds, upcomingEventCount };
198
+
199
+ //# sourceMappingURL=event-reports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-reports.js","names":[],"sources":["../../src/repositories/event-reports.ts"],"sourcesContent":["import { sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n EventAnalytics,\n EventAttendeeSource,\n MemberEventsQuery,\n Page,\n} from \"@headless-lms/core/types\";\nimport { eventSettingsDefaults } from \"@headless-lms/core/schemas\";\nimport type {\n EventAttendeeRow,\n EventFaces,\n EventReader,\n} from \"@headless-lms/core/reporting/events\";\nimport { oneOf } from \"./grant-covers.js\";\nimport { SETTINGS_NAMESPACE } from \"./content.js\";\n\nfunction attendance(orgId: string, eventIds: string[]): SQL {\n return sql`\n select ca.content_id, ca.org_user_id,\n max(case when e.content_id is not null then e.source end) as direct_source,\n min(case when e.content_id is not null then ca.granted_at end) as direct_at,\n min(ca.granted_at) as any_at\n from content_access ca\n join entitlements e on e.org_id = ca.org_id and e.id = ca.entitlement_id\n where ca.org_id = ${orgId} and ${oneOf(sql`ca.content_id`, eventIds)}\n group by ca.content_id, ca.org_user_id\n `;\n}\n\nconst DISPLAY_NAME = sql`coalesce(\n nullif(trim(up.display_name), ''),\n nullif(trim(concat_ws(' ', u.first_name, u.last_name)), '')\n)`;\n\nconst SOURCE = sql`case\n when a.direct_source in ('self', 'automation') then a.direct_source\n when a.direct_source is not null then 'manual'\n else 'bundle'\nend`;\n\nexport async function eventAttendeePage(\n db: NodePgDatabase,\n orgId: string,\n eventId: string,\n page: { page: number; pageSize: number },\n): Promise<Page<EventAttendeeRow>> {\n const result = await db.execute(sql`\n with a as (${attendance(orgId, [eventId])})\n select a.org_user_id,\n ${DISPLAY_NAME} as display_name,\n case when up.image_asset_org_id = ${orgId} then up.image_asset_id end as image_asset_id,\n ${SOURCE} as source,\n coalesce(a.direct_at, a.any_at) as granted_at,\n count(*) over () as total\n from a\n join org_users ou on ou.org_id = ${orgId} and ou.id = a.org_user_id\n left join users u on u.id = ou.user_id\n left join user_profiles up on up.user_id = ou.user_id\n order by coalesce(a.direct_at, a.any_at), a.org_user_id\n limit ${page.pageSize}\n offset ${(page.page - 1) * page.pageSize}\n `);\n const rows = result.rows as {\n org_user_id: string;\n display_name: string | null;\n image_asset_id: string | null;\n source: EventAttendeeSource;\n granted_at: Date | string;\n total: string | number;\n }[];\n const total =\n rows.length > 0 || page.page === 1\n ? Number(rows[0]?.total ?? 0)\n : await eventAttendeeCount(db, orgId, eventId);\n return {\n rows: rows.map((row) => ({\n orgUserId: row.org_user_id,\n displayName: row.display_name,\n imageAssetId: row.image_asset_id,\n source: row.source,\n grantedAt: new Date(row.granted_at),\n })),\n total,\n page: page.page,\n pageSize: page.pageSize,\n };\n}\n\nasync function eventAttendeeCount(\n db: NodePgDatabase,\n orgId: string,\n eventId: string,\n): Promise<number> {\n const result = await db.execute(sql`\n select count(distinct ca.org_user_id) as total\n from content_access ca\n where ca.org_id = ${orgId} and ca.content_id = ${eventId}\n `);\n return Number((result.rows[0] as { total: string | number } | undefined)?.total ?? 0);\n}\n\nexport async function eventAnalyticsFigures(\n db: NodePgDatabase,\n orgId: string,\n eventId: string,\n startsAt: Date,\n): Promise<Omit<EventAnalytics, \"capacity\">> {\n const [sources, revoked, overTime] = await Promise.all([\n db.execute(sql`\n with a as (${attendance(orgId, [eventId])})\n select ${SOURCE} as source, count(*) as count from a group by 1\n `),\n db.execute(sql`\n select count(*) as count from entitlements e\n where e.org_id = ${orgId} and e.content_id = ${eventId} and e.status = 'revoked'\n `),\n db.execute(sql`\n select to_char((e.granted_at at time zone 'utc')::date, 'YYYY-MM-DD') as date,\n count(*) as count\n from entitlements e\n where e.org_id = ${orgId}\n and e.content_id = ${eventId}\n and e.status = 'active'\n and (e.expires_at is null or e.expires_at > now())\n and e.granted_at < ${startsAt}\n group by 1\n order by 1\n `),\n ]);\n const bySource = { self: 0, manual: 0, automation: 0, bundle: 0 };\n for (const row of sources.rows as { source: EventAttendeeSource; count: string | number }[]) {\n bySource[row.source] = Number(row.count);\n }\n return {\n attendees: bySource.self + bySource.manual + bySource.automation + bySource.bundle,\n bySource,\n revoked: Number((revoked.rows[0] as { count: string | number } | undefined)?.count ?? 0),\n grantsOverTime: (overTime.rows as { date: string; count: string | number }[]).map((row) => ({\n date: row.date,\n count: Number(row.count),\n })),\n };\n}\n\nexport async function eventFacesFor(\n db: NodePgDatabase,\n orgId: string,\n eventIds: string[],\n limit: number,\n): Promise<Record<string, EventFaces>> {\n const faces: Record<string, EventFaces> = Object.fromEntries(\n eventIds.map((id) => [id, { total: 0, rows: [] }]),\n );\n if (eventIds.length === 0) {\n return faces;\n }\n const result = await db.execute(sql`\n with a as (${attendance(orgId, eventIds)}),\n ranked as (\n select a.content_id, a.org_user_id,\n ${DISPLAY_NAME} as display_name,\n case when up.image_asset_org_id = ${orgId} then up.image_asset_id end as image_asset_id,\n row_number() over (\n partition by a.content_id\n order by coalesce(a.direct_at, a.any_at), a.org_user_id\n ) as position,\n count(*) over (partition by a.content_id) as total\n from a\n join org_users ou on ou.org_id = ${orgId} and ou.id = a.org_user_id\n left join users u on u.id = ou.user_id\n left join user_profiles up on up.user_id = ou.user_id\n )\n select content_id, org_user_id, display_name, image_asset_id, total\n from ranked\n where position <= ${limit}\n order by content_id, position\n `);\n for (const row of result.rows as {\n content_id: string;\n org_user_id: string;\n display_name: string | null;\n image_asset_id: string | null;\n total: string | number;\n }[]) {\n const entry = faces[row.content_id];\n if (!entry) {\n continue;\n }\n entry.total = Number(row.total);\n entry.rows.push({\n orgUserId: row.org_user_id,\n displayName: row.display_name,\n imageAssetId: row.image_asset_id,\n });\n }\n return faces;\n}\n\nexport async function directEventGrantIds(\n db: NodePgDatabase,\n orgId: string,\n orgUserId: string,\n eventIds: string[],\n): Promise<string[]> {\n const result = await db.execute(sql`\n select distinct e.content_id from entitlements e\n where e.org_id = ${orgId}\n and e.org_user_id = ${orgUserId}\n and ${oneOf(sql`e.content_id`, eventIds)}\n and e.status = 'active'\n and (e.expires_at is null or e.expires_at > now())\n `);\n return (result.rows as { content_id: string }[]).map((row) => row.content_id);\n}\n\nexport async function memberEventIds(\n db: NodePgDatabase,\n orgId: string,\n query: MemberEventsQuery,\n reader: EventReader,\n now: Date,\n): Promise<Page<string>> {\n const attends = sql`exists (\n select 1 from content_access ca\n where ca.org_id = ev.org_id and ca.content_id = ev.id and ca.org_user_id = ${reader.orgUserId}\n )`;\n const sees = reader.readAll\n ? sql`true`\n : sql`(\n ${oneOf(sql`ev.id`, reader.readIds)}\n or (ev.status = 'published' and coalesce(s.value ->> 'type', ${eventSettingsDefaults.type}) = 'public')\n or ${attends}\n )`;\n const past = query.when === \"past\";\n const where = sql`\n ev.org_id = ${orgId}\n and ev.status in ('published', 'canceled')\n and ${sees}\n and ${query.attending ? attends : sql`true`}\n and ${past ? sql`ev.ends_at <= ${now}` : sql`ev.ends_at > ${now}`}\n `;\n const from = sql`\n from events ev\n left join settings s on s.org_id = ev.org_id and s.namespace = ${SETTINGS_NAMESPACE} and s.scope_id = ev.id\n `;\n const order = past ? sql`ev.starts_at desc, ev.id desc` : sql`ev.starts_at asc, ev.id asc`;\n const [rows, totals] = await Promise.all([\n db.execute(sql`\n select ev.id ${from} where ${where} order by ${order}\n limit ${query.pageSize} offset ${(query.page - 1) * query.pageSize}\n `),\n db.execute(sql`select count(*) as total ${from} where ${where}`),\n ]);\n return {\n rows: (rows.rows as { id: string }[]).map((row) => row.id),\n total: Number((totals.rows[0] as { total: string | number } | undefined)?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n}\n\nexport async function upcomingEventCount(db: NodePgDatabase, orgId: string): Promise<number> {\n const result = await db.execute(sql`\n select count(*) as total from events ev\n where ev.org_id = ${orgId} and ev.status = 'published' and ev.ends_at > now()\n `);\n return Number((result.rows[0] as { total: string | number } | undefined)?.total ?? 0);\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { entitlements } from "../schema/entitlements.js";
2
2
  import { SQL, SQLWrapper } from "drizzle-orm";
3
3
  //#region src/repositories/grant-covers.d.ts
4
- type GrantedContentType = "course" | "download" | "community";
4
+ type GrantedContentType = "course" | "download" | "community" | "event";
5
5
  interface GrantColumns {
6
6
  orgId: SQLWrapper;
7
7
  contentId: SQLWrapper;
@@ -14,7 +14,8 @@ declare function grantOpens(row: GrantColumns, contentId: SQL, type: GrantedCont
14
14
  declare function activeGrantCovers(orgId: SQL, orgUserId: SQL, contentId: SQL, type: GrantedContentType): SQL;
15
15
  declare function communityPublished(orgId: SQL, communityId: SQL): SQL;
16
16
  declare function holdsCommunity(orgId: SQL, orgUserId: SQL, communityId: SQL): SQL;
17
- declare function communityMember(orgId: SQL, orgUserId: SQL, role: SQL, communityId: SQL): SQL;
17
+ declare function oneOf(column: SQL, ids: readonly string[]): SQL;
18
+ declare function communityMember(orgId: SQL, orgUserId: SQL, communityId: SQL, readerIds: readonly string[]): SQL;
18
19
  //#endregion
19
- export { GrantColumns, GrantedContentType, activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity };
20
+ export { GrantColumns, GrantedContentType, activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity, oneOf };
20
21
  //# sourceMappingURL=grant-covers.d.ts.map
@@ -1,6 +1,5 @@
1
1
  import { entitlements } from "../schema/entitlements.js";
2
2
  import { sql } from "drizzle-orm";
3
- import { STAFF_ROLES } from "@headless-lms/core/organizations";
4
3
  //#region src/repositories/grant-covers.ts
5
4
  function grantUnexpired(expiresAt = entitlements.expiresAt, at = sql`now()`) {
6
5
  return sql`(${expiresAt} is null or ${expiresAt} > ${at})`;
@@ -46,10 +45,14 @@ function communityPublished(orgId, communityId) {
46
45
  function holdsCommunity(orgId, orgUserId, communityId) {
47
46
  return sql`(${communityPublished(orgId, communityId)} and ${activeGrantCovers(orgId, orgUserId, communityId, "community")})`;
48
47
  }
49
- function communityMember(orgId, orgUserId, role, communityId) {
50
- return sql`(${role} in (${sql.join(STAFF_ROLES.map((r) => sql`${r}`), sql`, `)}) or ${holdsCommunity(orgId, orgUserId, communityId)})`;
48
+ function oneOf(column, ids) {
49
+ if (ids.length === 0) return sql`false`;
50
+ return sql`${column} in (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`;
51
+ }
52
+ function communityMember(orgId, orgUserId, communityId, readerIds) {
53
+ return sql`(${oneOf(orgUserId, readerIds)} or ${holdsCommunity(orgId, orgUserId, communityId)})`;
51
54
  }
52
55
  //#endregion
53
- export { activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity };
56
+ export { activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity, oneOf };
54
57
 
55
58
  //# sourceMappingURL=grant-covers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grant-covers.js","names":[],"sources":["../../src/repositories/grant-covers.ts"],"sourcesContent":["import { sql, type SQL, type SQLWrapper } from \"drizzle-orm\";\nimport { STAFF_ROLES } from \"@headless-lms/core/organizations\";\nimport { entitlements } from \"../schema/entitlements.js\";\n\nexport type GrantedContentType = \"course\" | \"download\" | \"community\";\n\nexport interface GrantColumns {\n orgId: SQLWrapper;\n contentId: SQLWrapper;\n bundleId: SQLWrapper;\n}\n\nexport function grantUnexpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is null or ${expiresAt} > ${at})`;\n}\n\nexport function grantExpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is not null and ${expiresAt} <= ${at})`;\n}\n\nexport function grantIsActive(row: typeof entitlements = entitlements, at?: SQL): SQL {\n return sql`(${row.status} = 'active' and ${grantUnexpired(row.expiresAt, at)})`;\n}\n\nexport function grantOpens(row: GrantColumns, contentId: SQL, type: GrantedContentType): SQL {\n return sql`(\n exists (\n select 1 from content_items ci\n where ci.org_id = ${row.orgId} and ci.id = ${contentId} and ci.type = ${type}\n )\n and (\n ${row.contentId} = ${contentId}\n or exists (\n select 1 from bundle_items bi\n where bi.org_id = ${row.orgId}\n and bi.bundle_id = ${row.bundleId}\n and bi.content_id = ${contentId}\n )\n )\n )`;\n}\n\nexport function activeGrantCovers(\n orgId: SQL,\n orgUserId: SQL,\n contentId: SQL,\n type: GrantedContentType,\n): SQL {\n return sql`exists (\n select 1 from content_access ca\n join content_items ci on ci.org_id = ca.org_id and ci.id = ca.content_id and ci.type = ${type}\n where ca.org_id = ${orgId}\n and ca.org_user_id = ${orgUserId}\n and ca.content_id = ${contentId}\n )`;\n}\n\nexport function communityPublished(orgId: SQL, communityId: SQL): SQL {\n return sql`exists (\n select 1 from communities pc\n where pc.org_id = ${orgId} and pc.id = ${communityId} and pc.status = 'published'\n )`;\n}\n\nexport function holdsCommunity(orgId: SQL, orgUserId: SQL, communityId: SQL): SQL {\n return sql`(${communityPublished(orgId, communityId)} and ${activeGrantCovers(orgId, orgUserId, communityId, \"community\")})`;\n}\n\nexport function communityMember(orgId: SQL, orgUserId: SQL, role: SQL, communityId: SQL): SQL {\n const staff = sql.join(\n STAFF_ROLES.map((r) => sql`${r}`),\n sql`, `,\n );\n return sql`(${role} in (${staff}) or ${holdsCommunity(orgId, orgUserId, communityId)})`;\n}\n"],"mappings":";;;;AAYA,SAAgB,eACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,cAAc,UAAU,KAAK,GAAG;AAC1D;AAEA,SAAgB,aACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,mBAAmB,UAAU,MAAM,GAAG;AAChE;AAEA,SAAgB,cAAc,MAA2B,cAAc,IAAe;CACpF,OAAO,GAAG,IAAI,IAAI,OAAO,kBAAkB,eAAe,IAAI,WAAW,EAAE,EAAE;AAC/E;AAEA,SAAgB,WAAW,KAAmB,WAAgB,MAA+B;CAC3F,OAAO,GAAG;;;0BAGc,IAAI,MAAM,eAAe,UAAU,iBAAiB,KAAK;;;QAG3E,IAAI,UAAU,KAAK,UAAU;;;4BAGT,IAAI,MAAM;+BACP,IAAI,SAAS;gCACZ,UAAU;;;;AAI1C;AAEA,SAAgB,kBACd,OACA,WACA,WACA,MACK;CACL,OAAO,GAAG;;6FAEiF,KAAK;wBAC1E,MAAM;6BACD,UAAU;4BACX,UAAU;;AAEtC;AAEA,SAAgB,mBAAmB,OAAY,aAAuB;CACpE,OAAO,GAAG;;wBAEY,MAAM,eAAe,YAAY;;AAEzD;AAEA,SAAgB,eAAe,OAAY,WAAgB,aAAuB;CAChF,OAAO,GAAG,IAAI,mBAAmB,OAAO,WAAW,EAAE,OAAO,kBAAkB,OAAO,WAAW,aAAa,WAAW,EAAE;AAC5H;AAEA,SAAgB,gBAAgB,OAAY,WAAgB,MAAW,aAAuB;CAK5F,OAAO,GAAG,IAAI,KAAK,OAJL,IAAI,KAChB,YAAY,KAAK,MAAM,GAAG,GAAG,GAAG,GAChC,GAAG,IAEyB,EAAE,OAAO,eAAe,OAAO,WAAW,WAAW,EAAE;AACvF"}
1
+ {"version":3,"file":"grant-covers.js","names":[],"sources":["../../src/repositories/grant-covers.ts"],"sourcesContent":["import { sql, type SQL, type SQLWrapper } from \"drizzle-orm\";\nimport { entitlements } from \"../schema/entitlements.js\";\n\nexport type GrantedContentType = \"course\" | \"download\" | \"community\" | \"event\";\n\nexport interface GrantColumns {\n orgId: SQLWrapper;\n contentId: SQLWrapper;\n bundleId: SQLWrapper;\n}\n\nexport function grantUnexpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is null or ${expiresAt} > ${at})`;\n}\n\nexport function grantExpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is not null and ${expiresAt} <= ${at})`;\n}\n\nexport function grantIsActive(row: typeof entitlements = entitlements, at?: SQL): SQL {\n return sql`(${row.status} = 'active' and ${grantUnexpired(row.expiresAt, at)})`;\n}\n\nexport function grantOpens(row: GrantColumns, contentId: SQL, type: GrantedContentType): SQL {\n return sql`(\n exists (\n select 1 from content_items ci\n where ci.org_id = ${row.orgId} and ci.id = ${contentId} and ci.type = ${type}\n )\n and (\n ${row.contentId} = ${contentId}\n or exists (\n select 1 from bundle_items bi\n where bi.org_id = ${row.orgId}\n and bi.bundle_id = ${row.bundleId}\n and bi.content_id = ${contentId}\n )\n )\n )`;\n}\n\nexport function activeGrantCovers(\n orgId: SQL,\n orgUserId: SQL,\n contentId: SQL,\n type: GrantedContentType,\n): SQL {\n return sql`exists (\n select 1 from content_access ca\n join content_items ci on ci.org_id = ca.org_id and ci.id = ca.content_id and ci.type = ${type}\n where ca.org_id = ${orgId}\n and ca.org_user_id = ${orgUserId}\n and ca.content_id = ${contentId}\n )`;\n}\n\nexport function communityPublished(orgId: SQL, communityId: SQL): SQL {\n return sql`exists (\n select 1 from communities pc\n where pc.org_id = ${orgId} and pc.id = ${communityId} and pc.status = 'published'\n )`;\n}\n\nexport function holdsCommunity(orgId: SQL, orgUserId: SQL, communityId: SQL): SQL {\n return sql`(${communityPublished(orgId, communityId)} and ${activeGrantCovers(orgId, orgUserId, communityId, \"community\")})`;\n}\n\nexport function oneOf(column: SQL, ids: readonly string[]): SQL {\n if (ids.length === 0) {\n return sql`false`;\n }\n return sql`${column} in (${sql.join(\n ids.map((id) => sql`${id}`),\n sql`, `,\n )})`;\n}\n\nexport function communityMember(\n orgId: SQL,\n orgUserId: SQL,\n communityId: SQL,\n readerIds: readonly string[],\n): SQL {\n return sql`(${oneOf(orgUserId, readerIds)} or ${holdsCommunity(orgId, orgUserId, communityId)})`;\n}\n"],"mappings":";;;AAWA,SAAgB,eACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,cAAc,UAAU,KAAK,GAAG;AAC1D;AAEA,SAAgB,aACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,mBAAmB,UAAU,MAAM,GAAG;AAChE;AAEA,SAAgB,cAAc,MAA2B,cAAc,IAAe;CACpF,OAAO,GAAG,IAAI,IAAI,OAAO,kBAAkB,eAAe,IAAI,WAAW,EAAE,EAAE;AAC/E;AAEA,SAAgB,WAAW,KAAmB,WAAgB,MAA+B;CAC3F,OAAO,GAAG;;;0BAGc,IAAI,MAAM,eAAe,UAAU,iBAAiB,KAAK;;;QAG3E,IAAI,UAAU,KAAK,UAAU;;;4BAGT,IAAI,MAAM;+BACP,IAAI,SAAS;gCACZ,UAAU;;;;AAI1C;AAEA,SAAgB,kBACd,OACA,WACA,WACA,MACK;CACL,OAAO,GAAG;;6FAEiF,KAAK;wBAC1E,MAAM;6BACD,UAAU;4BACX,UAAU;;AAEtC;AAEA,SAAgB,mBAAmB,OAAY,aAAuB;CACpE,OAAO,GAAG;;wBAEY,MAAM,eAAe,YAAY;;AAEzD;AAEA,SAAgB,eAAe,OAAY,WAAgB,aAAuB;CAChF,OAAO,GAAG,IAAI,mBAAmB,OAAO,WAAW,EAAE,OAAO,kBAAkB,OAAO,WAAW,aAAa,WAAW,EAAE;AAC5H;AAEA,SAAgB,MAAM,QAAa,KAA6B;CAC9D,IAAI,IAAI,WAAW,GACjB,OAAO,GAAG;CAEZ,OAAO,GAAG,GAAG,OAAO,OAAO,IAAI,KAC7B,IAAI,KAAK,OAAO,GAAG,GAAG,IAAI,GAC1B,GAAG,IACL,EAAE;AACJ;AAEA,SAAgB,gBACd,OACA,WACA,aACA,WACK;CACL,OAAO,GAAG,IAAI,MAAM,WAAW,SAAS,EAAE,MAAM,eAAe,OAAO,WAAW,WAAW,EAAE;AAChG"}
@@ -1,7 +1,7 @@
1
1
  import { userProfiles, users } from "../schema/identity.js";
2
2
  import { translateDbErrors } from "./pg-errors.js";
3
- import { eq, sql } from "drizzle-orm";
4
3
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { eq, sql } from "drizzle-orm";
5
5
  //#region src/repositories/identity.ts
6
6
  var DrizzleIdentityRepository = class {
7
7
  db;
@@ -1,7 +1,7 @@
1
1
  import { connections } from "../schema/integrations.js";
2
2
  import { translateDbErrors } from "./pg-errors.js";
3
- import { and, asc, eq } from "drizzle-orm";
4
3
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { and, asc, eq } from "drizzle-orm";
5
5
  //#region src/repositories/integrations.ts
6
6
  function toConnection(row) {
7
7
  return {
@@ -1,15 +1,23 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Page } from "@headless-lms/core/types";
4
- import { DirectoryQuery, DirectoryRow, MemberReportRepository } from "@headless-lms/core/reporting/member";
3
+ import { MemberEventsQuery, Page } from "@headless-lms/core/types";
4
+ import { DirectoryQuery, DirectoryRow, EventAttendeeRow, EventReader, MemberReportRepository } from "@headless-lms/core/reporting/member";
5
+ import { EventFaces } from "@headless-lms/core/reporting/events";
5
6
  //#region src/repositories/member.d.ts
6
7
  declare class DrizzleMemberRepository implements MemberReportRepository {
7
8
  private readonly db;
8
9
  private readonly logger;
9
10
  constructor(db: NodePgDatabase, logger?: Logger);
10
- directory(orgId: string, communityId: string, query: DirectoryQuery): Promise<Page<DirectoryRow>>;
11
+ directory(orgId: string, communityId: string, query: DirectoryQuery, readerIds: string[]): Promise<Page<DirectoryRow>>;
11
12
  people(orgId: string, orgUserIds: string[]): Promise<DirectoryRow[]>;
12
- spaceMemberCounts(orgId: string, communityId: string): Promise<Record<string, number>>;
13
+ eventAttendees(orgId: string, eventId: string, page: {
14
+ page: number;
15
+ pageSize: number;
16
+ }): Promise<Page<EventAttendeeRow>>;
17
+ eventFaces(orgId: string, eventIds: string[], limit: number): Promise<Record<string, EventFaces>>;
18
+ directEventGrants(orgId: string, orgUserId: string, eventIds: string[]): Promise<string[]>;
19
+ events(orgId: string, query: MemberEventsQuery, reader: EventReader, now: Date): Promise<Page<string>>;
20
+ spaceMemberCounts(orgId: string, communityId: string, excluding: string[]): Promise<Record<string, number>>;
13
21
  }
14
22
  //#endregion
15
23
  export { DrizzleMemberRepository };
@@ -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);