@headless-lms/adapter-db 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +8 -2
- package/dist/client.js +10 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/repositories/admin.d.ts +12 -4
- package/dist/repositories/admin.js +164 -22
- package/dist/repositories/admin.js.map +1 -1
- package/dist/repositories/assets.d.ts +2 -1
- package/dist/repositories/assets.js +12 -1
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.js +1 -1
- package/dist/repositories/chats.d.ts +13 -4
- package/dist/repositories/chats.js +164 -70
- package/dist/repositories/chats.js.map +1 -1
- package/dist/repositories/content.d.ts +22 -5
- package/dist/repositories/content.js +201 -40
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/credentials.js +1 -1
- package/dist/repositories/devices.js +1 -1
- package/dist/repositories/digest-runs.js +1 -1
- package/dist/repositories/discussion.d.ts +1 -1
- package/dist/repositories/discussion.js +9 -3
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +10 -10
- package/dist/repositories/entitlements.js +47 -56
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/event-reports.d.ts +16 -0
- package/dist/repositories/event-reports.js +199 -0
- package/dist/repositories/event-reports.js.map +1 -0
- package/dist/repositories/grant-covers.d.ts +4 -3
- package/dist/repositories/grant-covers.js +7 -4
- package/dist/repositories/grant-covers.js.map +1 -1
- package/dist/repositories/identity.js +1 -1
- package/dist/repositories/integrations.js +1 -1
- package/dist/repositories/member.d.ts +12 -4
- package/dist/repositories/member.js +19 -7
- package/dist/repositories/member.js.map +1 -1
- package/dist/repositories/members.d.ts +2 -5
- package/dist/repositories/members.js +39 -94
- package/dist/repositories/members.js.map +1 -1
- package/dist/repositories/mentions.js +1 -1
- package/dist/repositories/notification-settings.js +1 -1
- package/dist/repositories/notifications.js +2 -2
- package/dist/repositories/notifications.js.map +1 -1
- package/dist/repositories/org-user-profile.d.ts +0 -17
- package/dist/repositories/org-user-profile.js +0 -1
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +4 -4
- package/dist/repositories/organizations.js +26 -46
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/outbox.d.ts +5 -1
- package/dist/repositories/outbox.js +12 -3
- package/dist/repositories/outbox.js.map +1 -1
- package/dist/repositories/posts.js +1 -1
- package/dist/repositories/progress.js +1 -1
- package/dist/repositories/roles.d.ts +35 -0
- package/dist/repositories/roles.js +244 -0
- package/dist/repositories/roles.js.map +1 -0
- package/dist/repositories/settings.js +1 -1
- package/dist/repositories/subscriptions.js +1 -1
- package/dist/schema/assets.d.ts +17 -0
- package/dist/schema/assets.js +1 -0
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +3 -3
- package/dist/schema/automations.js +1 -1
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/content.d.ts +488 -3
- package/dist/schema/content.js +89 -4
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/discussion.d.ts +17 -0
- package/dist/schema/discussion.js +4 -2
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +7 -2
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/organizations.d.ts +24 -22
- package/dist/schema/organizations.js +3 -14
- package/dist/schema/organizations.js.map +1 -1
- package/dist/schema/roles.d.ts +369 -0
- package/dist/schema/roles.js +71 -0
- package/dist/schema/roles.js.map +1 -0
- package/drizzle/0017_configurable_roles.sql +38 -0
- package/drizzle/0018_messages_client_id.sql +2 -0
- package/drizzle/0019_events.sql +44 -0
- package/drizzle/0020_space_images.sql +9 -0
- package/drizzle/0021_org_metadata.sql +1 -0
- package/drizzle/meta/0017_snapshot.json +9518 -0
- package/drizzle/meta/0018_snapshot.json +9557 -0
- package/drizzle/meta/0019_snapshot.json +9919 -0
- package/drizzle/meta/0020_snapshot.json +10045 -0
- package/drizzle/meta/0021_snapshot.json +10052 -0
- package/drizzle/meta/_journal.json +35 -0
- 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
|
|
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
|
|
50
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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`${
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
32
|
-
const
|
|
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
|
-
|
|
20
|
+
roleKey: invites.roleKey,
|
|
42
21
|
invitedAt: invites.createdAt
|
|
43
|
-
}).from(invites).where(and(eq(invites.orgId, orgId), eq(invites.status, "pending")
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|