@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
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { index_d_exports } from "./schema/index.js";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
4
|
//#region src/client.d.ts
|
|
4
5
|
/** Transaction executor — the same query surface as the root db.
|
|
5
6
|
* (Promoted from the structure.ts repository's local alias.) */
|
|
@@ -9,9 +10,14 @@ type Tx = Parameters<Parameters<NodePgDatabase["transaction"]>[0]>[0];
|
|
|
9
10
|
* statement escapes the transaction. */
|
|
10
11
|
type DbExecutor = NodePgDatabase | Tx;
|
|
11
12
|
type Db = ReturnType<typeof createDb>;
|
|
12
|
-
|
|
13
|
+
interface DatabasePoolOptions {
|
|
14
|
+
max?: number;
|
|
15
|
+
idleTimeoutMillis?: number;
|
|
16
|
+
connectionTimeoutMillis?: number;
|
|
17
|
+
}
|
|
18
|
+
declare function createDb(connectionString: string, logger?: Logger, poolOptions?: DatabasePoolOptions): NodePgDatabase<Record<string, never>> & {
|
|
13
19
|
$client: import("pg").Pool;
|
|
14
20
|
};
|
|
15
21
|
//#endregion
|
|
16
|
-
export { Db, DbExecutor, Tx, createDb, index_d_exports as schema };
|
|
22
|
+
export { DatabasePoolOptions, Db, DbExecutor, Tx, createDb, index_d_exports as schema };
|
|
17
23
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { schema_exports } from "./schema/index.js";
|
|
2
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
3
|
import pg from "pg";
|
|
4
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/client.ts
|
|
5
|
-
function createDb(connectionString) {
|
|
6
|
-
|
|
6
|
+
function createDb(connectionString, logger = noopLogger, poolOptions = {}) {
|
|
7
|
+
const pool = new pg.Pool({
|
|
8
|
+
connectionString,
|
|
9
|
+
...poolOptions
|
|
10
|
+
});
|
|
11
|
+
pool.on("error", (error) => {
|
|
12
|
+
logger.warn("database connection lost", { error: error.message });
|
|
13
|
+
});
|
|
14
|
+
return drizzle(pool);
|
|
7
15
|
}
|
|
8
16
|
//#endregion
|
|
9
17
|
export { createDb, schema_exports as schema };
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport pg from \"pg\";\n\n/** Transaction executor — the same query surface as the root db.\n * (Promoted from the structure.ts repository's local alias.) */\nexport type Tx = Parameters<Parameters<NodePgDatabase[\"transaction\"]>[0]>[0];\n\n/** What a Drizzle repository executes against: the root db OR a transaction\n * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no\n * statement escapes the transaction. */\nexport type DbExecutor = NodePgDatabase | Tx;\n\n// Centralized schema — re-exported so the db adapter is the single source for tables.\nexport * as schema from \"./schema/index.js\";\n\nexport type Db = ReturnType<typeof createDb>;\n\nexport function createDb(connectionString: string) {\n const pool = new pg.Pool({ connectionString });\n return drizzle(pool);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport pg from \"pg\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\n\n/** Transaction executor — the same query surface as the root db.\n * (Promoted from the structure.ts repository's local alias.) */\nexport type Tx = Parameters<Parameters<NodePgDatabase[\"transaction\"]>[0]>[0];\n\n/** What a Drizzle repository executes against: the root db OR a transaction\n * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no\n * statement escapes the transaction. */\nexport type DbExecutor = NodePgDatabase | Tx;\n\n// Centralized schema — re-exported so the db adapter is the single source for tables.\nexport * as schema from \"./schema/index.js\";\n\nexport type Db = ReturnType<typeof createDb>;\n\nexport interface DatabasePoolOptions {\n max?: number;\n idleTimeoutMillis?: number;\n connectionTimeoutMillis?: number;\n}\n\nexport function createDb(\n connectionString: string,\n logger: Logger = noopLogger,\n poolOptions: DatabasePoolOptions = {},\n) {\n const pool = new pg.Pool({ connectionString, ...poolOptions });\n pool.on(\"error\", (error) => {\n logger.warn(\"database connection lost\", { error: error.message });\n });\n return drizzle(pool);\n}\n"],"mappings":";;;;;AA0BA,SAAgB,SACd,kBACA,SAAiB,YACjB,cAAmC,CAAC,GACpC;CACA,MAAM,OAAO,IAAI,GAAG,KAAK;EAAE;EAAkB,GAAG;CAAY,CAAC;CAC7D,KAAK,GAAG,UAAU,UAAU;EAC1B,OAAO,KAAK,4BAA4B,EAAE,OAAO,MAAM,QAAQ,CAAC;CAClE,CAAC;CACD,OAAO,QAAQ,IAAI;AACrB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { index_d_exports } from "./schema/index.js";
|
|
2
|
-
import { Db, createDb } from "./client.js";
|
|
2
|
+
import { DatabasePoolOptions, Db, createDb } from "./client.js";
|
|
3
3
|
import { DrizzleUnitOfWork } from "./unit-of-work.js";
|
|
4
4
|
import { migrationsFolder, runMigrations } from "./migrate.js";
|
|
5
5
|
import { DrizzleAssetsRepository } from "./repositories/assets.js";
|
|
6
6
|
import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
|
|
7
|
-
import { DrizzleContentRepository } from "./repositories/content.js";
|
|
7
|
+
import { DrizzleContentRepository, SETTINGS_NAMESPACE } from "./repositories/content.js";
|
|
8
8
|
import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
|
|
9
9
|
import { DrizzleDiscussionRepository } from "./repositories/discussion.js";
|
|
10
10
|
import { DrizzleDigestRunRepository } from "./repositories/digest-runs.js";
|
|
@@ -21,9 +21,10 @@ import { DrizzleNotificationSettingsRepository } from "./repositories/notificati
|
|
|
21
21
|
import { DrizzleDevicesRepository } from "./repositories/devices.js";
|
|
22
22
|
import { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./repositories/org-user-profile.js";
|
|
23
23
|
import { DrizzleOrganizationsRepository } from "./repositories/organizations.js";
|
|
24
|
-
import {
|
|
24
|
+
import { DrizzleRolesRepository } from "./repositories/roles.js";
|
|
25
|
+
import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS } from "./repositories/outbox.js";
|
|
25
26
|
import { DrizzleProgressRepository } from "./repositories/progress.js";
|
|
26
27
|
import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
|
|
27
28
|
import { DrizzleSubscriptionsRepository } from "./repositories/subscriptions.js";
|
|
28
29
|
import { DrizzleAdminRepository } from "./repositories/admin.js";
|
|
29
|
-
export { type Db, DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDevicesRepository, DrizzleDigestRunRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleMentionsRepository, DrizzleNotificationSettingsRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleSubscriptionsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, index_d_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
|
30
|
+
export { type DatabasePoolOptions, type Db, DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDevicesRepository, DrizzleDigestRunRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleMentionsRepository, DrizzleNotificationSettingsRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleRolesRepository, DrizzleSettingsRepository, DrizzleSubscriptionsRepository, DrizzleUnitOfWork, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS, SETTINGS_NAMESPACE, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, index_d_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { migrationsFolder, runMigrations } from "./migrate.js";
|
|
|
5
5
|
import { DrizzleAssetsRepository } from "./repositories/assets.js";
|
|
6
6
|
import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
|
|
7
7
|
import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
|
|
8
|
-
import { DrizzleContentRepository } from "./repositories/content.js";
|
|
8
|
+
import { DrizzleContentRepository, SETTINGS_NAMESPACE } from "./repositories/content.js";
|
|
9
9
|
import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
|
|
10
10
|
import { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./repositories/org-user-profile.js";
|
|
11
11
|
import { DrizzleDiscussionRepository } from "./repositories/discussion.js";
|
|
@@ -22,8 +22,9 @@ import { DrizzleNotificationsRepository } from "./repositories/notifications.js"
|
|
|
22
22
|
import { DrizzleNotificationSettingsRepository } from "./repositories/notification-settings.js";
|
|
23
23
|
import { DrizzleDevicesRepository } from "./repositories/devices.js";
|
|
24
24
|
import { DrizzleOrganizationsRepository } from "./repositories/organizations.js";
|
|
25
|
-
import {
|
|
25
|
+
import { DrizzleRolesRepository } from "./repositories/roles.js";
|
|
26
|
+
import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS } from "./repositories/outbox.js";
|
|
26
27
|
import { DrizzleProgressRepository } from "./repositories/progress.js";
|
|
27
28
|
import { DrizzleSubscriptionsRepository } from "./repositories/subscriptions.js";
|
|
28
29
|
import { DrizzleAdminRepository } from "./repositories/admin.js";
|
|
29
|
-
export { DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDevicesRepository, DrizzleDigestRunRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleMentionsRepository, DrizzleNotificationSettingsRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleSubscriptionsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
|
30
|
+
export { DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDevicesRepository, DrizzleDigestRunRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleMentionsRepository, DrizzleNotificationSettingsRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleRolesRepository, DrizzleSettingsRepository, DrizzleSubscriptionsRepository, DrizzleUnitOfWork, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS, SETTINGS_NAMESPACE, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
|
-
import {
|
|
3
|
+
import { OrgUserProfile } from "@headless-lms/core/types";
|
|
4
|
+
import { AdminReportRepository, CommunityAnalytics, CommunityMembersQuery, CourseAnalytics, CourseEnrollmentPoint, CourseLearner, CourseLearnersQuery, EnrollmentPoint, EventAnalytics as EventAnalytics$1, EventAttendeeRow, OverviewStats, Page as Page$1, StudentAnalytics, StudentRow, StudentsQuery } from "@headless-lms/core/reporting/admin";
|
|
4
5
|
//#region src/repositories/admin.d.ts
|
|
5
6
|
declare class DrizzleAdminRepository implements AdminReportRepository {
|
|
6
7
|
private readonly db;
|
|
@@ -11,12 +12,19 @@ declare class DrizzleAdminRepository implements AdminReportRepository {
|
|
|
11
12
|
private courseExists;
|
|
12
13
|
courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
13
14
|
courseEnrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
courseLearners(orgId: string, courseId: string, query: CourseLearnersQuery): Promise<Page$1<CourseLearner> | null>;
|
|
16
|
+
listStudents(orgId: string, query: StudentsQuery, memberRoleKey: string): Promise<Page$1<StudentRow>>;
|
|
17
|
+
getStudent(orgId: string, id: string): Promise<StudentRow | null>;
|
|
18
|
+
private itemsOf;
|
|
16
19
|
studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
17
20
|
private resolveOrder;
|
|
18
21
|
communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null>;
|
|
19
|
-
communityMembers(orgId: string, communityId: string, query: CommunityMembersQuery): Promise<Page<
|
|
22
|
+
communityMembers(orgId: string, communityId: string, query: CommunityMembersQuery): Promise<Page$1<OrgUserProfile> | null>;
|
|
23
|
+
eventAttendees(orgId: string, eventId: string, page: {
|
|
24
|
+
page: number;
|
|
25
|
+
pageSize: number;
|
|
26
|
+
}): Promise<Page$1<EventAttendeeRow>>;
|
|
27
|
+
eventAnalytics(orgId: string, eventId: string, startsAt: Date): Promise<Omit<EventAnalytics$1, "capacity">>;
|
|
20
28
|
}
|
|
21
29
|
//#endregion
|
|
22
30
|
export { DrizzleAdminRepository };
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
import { communities, courses, spaces } from "../schema/content.js";
|
|
2
2
|
import { users } from "../schema/identity.js";
|
|
3
|
-
import { orgUsers } from "../schema/organizations.js";
|
|
3
|
+
import { invites, orgUsers } from "../schema/organizations.js";
|
|
4
4
|
import { contentAccess, entitlements } from "../schema/entitlements.js";
|
|
5
5
|
import { commentFlags, comments, messageFlags, messages, postFlags, posts } from "../schema/discussion.js";
|
|
6
|
+
import { roleAssignments } from "../schema/roles.js";
|
|
6
7
|
import "../schema/index.js";
|
|
7
8
|
import { translateDbErrors } from "./pg-errors.js";
|
|
8
9
|
import { user } from "../schema/better-auth.js";
|
|
9
10
|
import { orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./org-user-profile.js";
|
|
10
11
|
import { grantExpired, grantOpens } from "./grant-covers.js";
|
|
12
|
+
import { eventAnalyticsFigures, eventAttendeePage, upcomingEventCount } from "./event-reports.js";
|
|
13
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
11
14
|
import { and, asc, desc, eq, exists, ilike, inArray, or, sql } from "drizzle-orm";
|
|
12
15
|
import { union } from "drizzle-orm/pg-core";
|
|
13
|
-
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
14
|
-
import { STUDENT_ROLE } from "@headless-lms/core/organizations";
|
|
15
16
|
//#region src/repositories/admin.ts
|
|
16
17
|
const entitlementCountExpr = sql`count(${entitlements.id})`;
|
|
17
18
|
const avgProgressExpr = sql`0`;
|
|
19
|
+
function hasNoOrgRole() {
|
|
20
|
+
return sql`not exists (
|
|
21
|
+
select 1 from ${roleAssignments} ra
|
|
22
|
+
where ra.org_id = ${orgUsers.orgId} and ra.org_user_id = ${orgUsers.id} and ra.content_id is null
|
|
23
|
+
)`;
|
|
24
|
+
}
|
|
25
|
+
function invitedAs(roleKey) {
|
|
26
|
+
return sql`not exists (
|
|
27
|
+
select 1 from ${invites} i
|
|
28
|
+
where i.org_id = ${orgUsers.orgId} and lower(i.email) = lower(${users.email})
|
|
29
|
+
and i.status = 'pending' and i.role <> ${roleKey}
|
|
30
|
+
)`;
|
|
31
|
+
}
|
|
32
|
+
function isStudent(memberRoleKey) {
|
|
33
|
+
return and(hasNoOrgRole(), or(eq(orgUsers.status, "active"), and(eq(orgUsers.status, "invited"), invitedAs(memberRoleKey))));
|
|
34
|
+
}
|
|
18
35
|
const lastActiveExpr = sql`(
|
|
19
36
|
select max(pr.updated_at) from progress_records pr
|
|
20
37
|
where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}
|
|
21
38
|
)`;
|
|
22
|
-
function toStudent(row) {
|
|
39
|
+
function toStudent(row, items) {
|
|
23
40
|
return {
|
|
41
|
+
items,
|
|
24
42
|
id: row.id,
|
|
25
43
|
email: row.email,
|
|
26
44
|
image: row.image ?? null,
|
|
@@ -53,7 +71,8 @@ var DrizzleAdminRepository = class {
|
|
|
53
71
|
publishedCourses: Number(courseCounts?.published ?? 0),
|
|
54
72
|
draftCourses: Number(courseCounts?.draft ?? 0),
|
|
55
73
|
activeStudents: Number(entitlementCounts?.activeStudents ?? 0),
|
|
56
|
-
expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0)
|
|
74
|
+
expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0),
|
|
75
|
+
upcomingEvents: await upcomingEventCount(this.db, orgId)
|
|
57
76
|
};
|
|
58
77
|
}
|
|
59
78
|
async enrollments(orgId, days) {
|
|
@@ -191,8 +210,106 @@ var DrizzleAdminRepository = class {
|
|
|
191
210
|
count: Number(r.count)
|
|
192
211
|
}));
|
|
193
212
|
}
|
|
194
|
-
async
|
|
195
|
-
|
|
213
|
+
async courseLearners(orgId, courseId, query) {
|
|
214
|
+
if (!await this.courseExists(orgId, courseId)) return null;
|
|
215
|
+
const expired = grantExpired(entitlements.expiresAt);
|
|
216
|
+
const grantStatus = sql`case
|
|
217
|
+
when ${entitlements.status} = 'revoked' then 'revoked'
|
|
218
|
+
when ${expired} then 'expired'
|
|
219
|
+
else 'active'
|
|
220
|
+
end`;
|
|
221
|
+
const grantRank = sql`case
|
|
222
|
+
when ${entitlements.status} = 'revoked' then 2
|
|
223
|
+
when ${expired} then 1
|
|
224
|
+
else 0
|
|
225
|
+
end`;
|
|
226
|
+
const learnerGrant = this.db.$with("learner_grant").as(this.db.selectDistinctOn([entitlements.orgUserId], {
|
|
227
|
+
orgUserId: entitlements.orgUserId,
|
|
228
|
+
status: grantStatus.as("grant_status"),
|
|
229
|
+
source: entitlements.source,
|
|
230
|
+
grantedAt: entitlements.grantedAt,
|
|
231
|
+
expiresAt: entitlements.expiresAt
|
|
232
|
+
}).from(entitlements).where(and(eq(entitlements.orgId, orgId), grantOpens(entitlements, sql`${courseId}`, "course"))).orderBy(entitlements.orgUserId, grantRank, sql`${entitlements.expiresAt} desc nulls first`, desc(entitlements.grantedAt)));
|
|
233
|
+
const publishedActivities = sql`(
|
|
234
|
+
select a.id from activities a
|
|
235
|
+
where a.org_id = ${orgId}
|
|
236
|
+
and a.course_id = ${courseId}
|
|
237
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
238
|
+
)`;
|
|
239
|
+
const completedActivities = sql`(
|
|
240
|
+
select count(*) from progress_records pr
|
|
241
|
+
where pr.org_id = ${orgId}
|
|
242
|
+
and pr.org_user_id = ${orgUsers.id}
|
|
243
|
+
and pr.target_type = 'activity'
|
|
244
|
+
and pr.completed_at is not null
|
|
245
|
+
and pr.target_id in ${publishedActivities}
|
|
246
|
+
)`;
|
|
247
|
+
const lastActivityAt = sql`(
|
|
248
|
+
select max(pr.updated_at) from progress_records pr
|
|
249
|
+
where pr.org_id = ${orgId}
|
|
250
|
+
and pr.org_user_id = ${orgUsers.id}
|
|
251
|
+
and (
|
|
252
|
+
(pr.target_type = 'activity' and pr.target_id in ${publishedActivities})
|
|
253
|
+
or (pr.target_type = 'course' and pr.target_id = ${courseId})
|
|
254
|
+
)
|
|
255
|
+
)`;
|
|
256
|
+
const completedAt = sql`(
|
|
257
|
+
select pr.completed_at from progress_records pr
|
|
258
|
+
where pr.org_id = ${orgId}
|
|
259
|
+
and pr.org_user_id = ${orgUsers.id}
|
|
260
|
+
and pr.target_type = 'course'
|
|
261
|
+
and pr.target_id = ${courseId}
|
|
262
|
+
)`;
|
|
263
|
+
const filters = [eq(orgUsers.orgId, orgId)];
|
|
264
|
+
const q = query.search?.trim();
|
|
265
|
+
if (q) {
|
|
266
|
+
const like = `%${q}%`;
|
|
267
|
+
filters.push(or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like)));
|
|
268
|
+
}
|
|
269
|
+
const where = and(...filters);
|
|
270
|
+
const learner = and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, learnerGrant.orgUserId));
|
|
271
|
+
const [[totals], [activityCount], rows] = await Promise.all([
|
|
272
|
+
this.db.with(learnerGrant).select({ total: sql`count(*)` }).from(learnerGrant).innerJoin(orgUsers, learner).innerJoin(users, eq(users.id, orgUsers.userId)).where(where),
|
|
273
|
+
this.db.select({ total: sql`count(*)` }).from(sql`${publishedActivities} as published_activities`),
|
|
274
|
+
this.db.with(learnerGrant).select({
|
|
275
|
+
...studentProfileColumns,
|
|
276
|
+
status: learnerGrant.status,
|
|
277
|
+
source: learnerGrant.source,
|
|
278
|
+
grantedAt: learnerGrant.grantedAt,
|
|
279
|
+
expiresAt: learnerGrant.expiresAt,
|
|
280
|
+
completedActivities,
|
|
281
|
+
lastActivityAt,
|
|
282
|
+
completedAt
|
|
283
|
+
}).from(learnerGrant).innerJoin(orgUsers, learner).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).orderBy(asc(studentFirstName), asc(studentLastName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)
|
|
284
|
+
]);
|
|
285
|
+
const totalActivities = Number(activityCount?.total ?? 0);
|
|
286
|
+
return {
|
|
287
|
+
rows: rows.map((row) => {
|
|
288
|
+
const done = Number(row.completedActivities);
|
|
289
|
+
return {
|
|
290
|
+
id: row.id,
|
|
291
|
+
firstName: row.firstName,
|
|
292
|
+
lastName: row.lastName,
|
|
293
|
+
email: row.email,
|
|
294
|
+
image: row.image ?? null,
|
|
295
|
+
status: row.status,
|
|
296
|
+
source: row.source,
|
|
297
|
+
grantedAt: new Date(row.grantedAt).toISOString(),
|
|
298
|
+
expiresAt: row.expiresAt ? new Date(row.expiresAt).toISOString() : null,
|
|
299
|
+
totalActivities,
|
|
300
|
+
completedActivities: done,
|
|
301
|
+
progress: totalActivities > 0 ? Math.round(Math.min(done / totalActivities, 1) * 100) : 0,
|
|
302
|
+
lastActivityAt: row.lastActivityAt ? new Date(row.lastActivityAt).toISOString() : null,
|
|
303
|
+
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : null
|
|
304
|
+
};
|
|
305
|
+
}),
|
|
306
|
+
total: Number(totals?.total ?? 0),
|
|
307
|
+
page: query.page,
|
|
308
|
+
pageSize: query.pageSize
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async listStudents(orgId, query, memberRoleKey) {
|
|
312
|
+
const filters = [eq(orgUsers.orgId, orgId), isStudent(memberRoleKey)];
|
|
196
313
|
const q = query.search?.trim();
|
|
197
314
|
if (q) {
|
|
198
315
|
const like = `%${q}%`;
|
|
@@ -200,15 +317,17 @@ var DrizzleAdminRepository = class {
|
|
|
200
317
|
}
|
|
201
318
|
const where = and(...filters);
|
|
202
319
|
const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
|
|
320
|
+
const rows = await this.db.select({
|
|
321
|
+
...studentProfileColumns,
|
|
322
|
+
status: orgUsers.status,
|
|
323
|
+
createdAt: orgUsers.createdAt,
|
|
324
|
+
entitlementCount: entitlementCountExpr,
|
|
325
|
+
avgProgress: avgProgressExpr,
|
|
326
|
+
lastActiveAt: lastActiveExpr
|
|
327
|
+
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.id, users.externalId, users.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
328
|
+
const items = await this.itemsOf(orgId, rows.map((row) => row.id));
|
|
203
329
|
return {
|
|
204
|
-
rows: (
|
|
205
|
-
...studentProfileColumns,
|
|
206
|
-
status: orgUsers.status,
|
|
207
|
-
createdAt: orgUsers.createdAt,
|
|
208
|
-
entitlementCount: entitlementCountExpr,
|
|
209
|
-
avgProgress: avgProgressExpr,
|
|
210
|
-
lastActiveAt: lastActiveExpr
|
|
211
|
-
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.id, users.externalId, users.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map(toStudent),
|
|
330
|
+
rows: rows.map((row) => toStudent(row, items.get(row.id) ?? [])),
|
|
212
331
|
total: Number(totals?.total ?? 0),
|
|
213
332
|
page: query.page,
|
|
214
333
|
pageSize: query.pageSize
|
|
@@ -222,11 +341,31 @@ var DrizzleAdminRepository = class {
|
|
|
222
341
|
entitlementCount: entitlementCountExpr,
|
|
223
342
|
avgProgress: avgProgressExpr,
|
|
224
343
|
lastActiveAt: lastActiveExpr
|
|
225
|
-
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)
|
|
226
|
-
|
|
344
|
+
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.id, users.externalId, users.email, users.firstName, users.lastName, user.image).limit(1);
|
|
345
|
+
if (!row) return null;
|
|
346
|
+
return toStudent(row, (await this.itemsOf(orgId, [row.id])).get(row.id) ?? []);
|
|
347
|
+
}
|
|
348
|
+
async itemsOf(orgId, orgUserIds) {
|
|
349
|
+
const byPerson = /* @__PURE__ */ new Map();
|
|
350
|
+
if (orgUserIds.length === 0) return byPerson;
|
|
351
|
+
const rows = await this.db.select({
|
|
352
|
+
orgUserId: roleAssignments.orgUserId,
|
|
353
|
+
contentId: roleAssignments.contentId,
|
|
354
|
+
contentType: roleAssignments.contentType
|
|
355
|
+
}).from(roleAssignments).where(and(eq(roleAssignments.orgId, orgId), inArray(roleAssignments.orgUserId, orgUserIds), sql`${roleAssignments.contentId} is not null`)).orderBy(asc(roleAssignments.createdAt));
|
|
356
|
+
for (const row of rows) {
|
|
357
|
+
if (row.contentId === null || row.contentType === null) continue;
|
|
358
|
+
const items = byPerson.get(row.orgUserId) ?? [];
|
|
359
|
+
items.push({
|
|
360
|
+
contentId: row.contentId,
|
|
361
|
+
contentType: row.contentType
|
|
362
|
+
});
|
|
363
|
+
byPerson.set(row.orgUserId, items);
|
|
364
|
+
}
|
|
365
|
+
return byPerson;
|
|
227
366
|
}
|
|
228
367
|
async studentAnalytics(orgId, id) {
|
|
229
|
-
const [exists] = await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)
|
|
368
|
+
const [exists] = await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)));
|
|
230
369
|
if (!exists) return null;
|
|
231
370
|
const courses = (await this.db.execute(sql`
|
|
232
371
|
with cohort_courses as (
|
|
@@ -373,10 +512,7 @@ var DrizzleAdminRepository = class {
|
|
|
373
512
|
const where = and(...filters);
|
|
374
513
|
const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
|
|
375
514
|
return {
|
|
376
|
-
rows: (await this.db.select({
|
|
377
|
-
...orgUserProfileColumns,
|
|
378
|
-
role: orgUsers.role
|
|
379
|
-
}).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).orderBy(asc(users.firstName), asc(users.lastName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map((row) => ({
|
|
515
|
+
rows: (await this.db.select(orgUserProfileColumns).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).orderBy(asc(users.firstName), asc(users.lastName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map((row) => ({
|
|
380
516
|
...row,
|
|
381
517
|
image: row.image ?? null
|
|
382
518
|
})),
|
|
@@ -385,6 +521,12 @@ var DrizzleAdminRepository = class {
|
|
|
385
521
|
pageSize: query.pageSize
|
|
386
522
|
};
|
|
387
523
|
}
|
|
524
|
+
eventAttendees(orgId, eventId, page) {
|
|
525
|
+
return eventAttendeePage(this.db, orgId, eventId, page);
|
|
526
|
+
}
|
|
527
|
+
eventAnalytics(orgId, eventId, startsAt) {
|
|
528
|
+
return eventAnalyticsFigures(this.db, orgId, eventId, startsAt);
|
|
529
|
+
}
|
|
388
530
|
};
|
|
389
531
|
translateDbErrors(DrizzleAdminRepository);
|
|
390
532
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.js","names":[],"sources":["../../src/repositories/admin.ts"],"sourcesContent":["// reporting/admin — Drizzle repository.\nimport { and, asc, desc, eq, exists, ilike, inArray, or, sql, type SQL } from \"drizzle-orm\";\nimport { union } from \"drizzle-orm/pg-core\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n AdminReportRepository,\n CommunityAnalytics,\n CommunityMember,\n CommunityMembersQuery,\n CourseActivityEngagement,\n CourseAnalytics,\n CourseEnrollmentPoint,\n EnrollmentPoint,\n OverviewStats,\n Page,\n Student,\n StudentAnalytics,\n StudentCourseProgress,\n StudentsQuery,\n} from \"@headless-lms/core/reporting/admin\";\nimport {\n commentFlags,\n comments,\n communities,\n contentAccess,\n courses,\n entitlements,\n messageFlags,\n messages,\n orgUsers,\n postFlags,\n posts,\n spaces,\n users,\n} from \"../schema/index.js\";\nimport { STUDENT_ROLE } from \"@headless-lms/core/organizations\";\nimport { user } from \"../schema/better-auth.js\";\nimport type { OrgUserStatus } from \"@headless-lms/core/types\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport {\n orgUserProfileColumns,\n studentFirstName,\n studentLastName,\n studentProfileColumns,\n} from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { grantExpired, grantOpens } from \"./grant-covers.js\";\n\nconst entitlementCountExpr = sql<number>`count(${entitlements.id})`;\n// Completion now lives in the progress domain; the students report no longer\n// derives a percentage from entitlements. Placeholder until wired to progress.\nconst avgProgressExpr = sql<number>`0`;\n// Last learning activity: the newest touch on any of the student's progress\n// records. A scalar subquery so it neither fans out the entitlement count nor\n// widens the GROUP BY.\nconst lastActiveExpr = sql<Date | null>`(\n select max(pr.updated_at) from progress_records pr\n where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}\n)`;\n\ninterface StudentRow {\n id: string;\n email: string;\n image: string | null;\n firstName: string | null;\n lastName: string | null;\n status: OrgUserStatus;\n createdAt: Date;\n entitlementCount: number;\n avgProgress: number;\n lastActiveAt: Date | null;\n}\n\nfunction toStudent(row: StudentRow): Student {\n return {\n id: row.id,\n email: row.email,\n image: row.image ?? null,\n firstName: row.firstName,\n lastName: row.lastName,\n status: row.status,\n entitlementCount: Number(row.entitlementCount),\n avgProgress: Number(row.avgProgress),\n joinedAt: row.createdAt.toISOString(),\n lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null,\n };\n}\n\nexport class DrizzleAdminRepository implements AdminReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async overview(orgId: string): Promise<OverviewStats> {\n const [courseCounts] = await this.db\n .select({\n published: sql<number>`count(*) filter (where ${courses.status} = 'published')`,\n draft: sql<number>`count(*) filter (where ${courses.status} = 'draft')`,\n })\n .from(courses)\n .where(eq(courses.orgId, orgId));\n\n const [entitlementCounts] = await this.db\n .select({\n activeStudents: sql<number>`count(distinct ${contentAccess.orgUserId})`,\n expiringSoon: sql<number>`count(distinct ${contentAccess.entitlementId}) filter (where ${grantExpired(contentAccess.expiresAt, sql`now() + interval '14 days'`)})`,\n })\n .from(contentAccess)\n .where(eq(contentAccess.orgId, orgId));\n\n return {\n publishedCourses: Number(courseCounts?.published ?? 0),\n draftCourses: Number(courseCounts?.draft ?? 0),\n activeStudents: Number(entitlementCounts?.activeStudents ?? 0),\n expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0),\n };\n }\n\n // Grants per day over the trailing window, zero-filled so the series has one\n // point per calendar day (UTC) even when nothing was granted.\n async enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]> {\n const result = await this.db.execute(sql`\n select\n to_char(d.day, 'YYYY-MM-DD') as date,\n count(${entitlements.id})::int as count\n from generate_series(\n (now() at time zone 'utc')::date - (${days} - 1),\n (now() at time zone 'utc')::date,\n interval '1 day'\n ) as d(day)\n left join ${entitlements}\n on ${entitlements.orgId} = ${orgId}\n and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date\n group by d.day\n order by d.day\n `);\n const rows = result.rows as { date: string; count: number }[];\n return rows.map((r) => ({ date: r.date, count: Number(r.count) }));\n }\n\n private async courseExists(orgId: string, courseId: string): Promise<boolean> {\n const [row] = await this.db\n .select({ id: courses.id })\n .from(courses)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));\n return row !== undefined;\n }\n\n async courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null> {\n if (!(await this.courseExists(orgId, courseId))) {\n return null;\n }\n\n const cohort = sql`\n select distinct org_user_id from content_access\n where org_id = ${orgId}\n and content_id = ${courseId}\n `;\n const publishedActivities = sql`\n select a.id, a.settings, a.seq, a.module_id\n from activities a\n where a.org_id = ${orgId}\n and a.course_id = ${courseId}\n and coalesce((a.settings ->> 'published')::boolean, true)\n `;\n\n const summaryResult = await this.db.execute(sql`\n with cohort as (${cohort}),\n acts as (${publishedActivities}),\n per_learner as (\n select c.org_user_id,\n count(pr.id) filter (where pr.completed_at is not null) as done\n from cohort c\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.org_user_id = c.org_user_id\n and pr.target_type = 'activity'\n and pr.target_id in (select id from acts)\n group by c.org_user_id\n )\n select\n (select count(*) from cohort)::int as enrolled,\n (\n select count(distinct pr.org_user_id) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id in (select org_user_id from cohort)\n and (\n (pr.target_type = 'activity' and pr.target_id in (select id from acts))\n or (pr.target_type = 'course' and pr.target_id = ${courseId})\n )\n )::int as started,\n (\n select count(*) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id in (select org_user_id from cohort)\n and pr.target_type = 'course'\n and pr.target_id = ${courseId}\n and pr.completed_at is not null\n )::int as completed,\n coalesce((\n select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))\n from per_learner\n ), 0)::int as avg_progress\n `);\n const summary = summaryResult.rows[0] as {\n enrolled: number;\n started: number;\n completed: number;\n avg_progress: number;\n };\n\n const funnelResult = await this.db.execute(sql`\n with cohort as (${cohort})\n select\n a.id,\n coalesce(a.settings ->> 'title', '') as title,\n m.id as module_id,\n m.title as module_title,\n count(pr.id)::int as started,\n count(pr.id) filter (where pr.completed_at is not null)::int as completed\n from activities a\n join modules m on m.org_id = a.org_id and m.id = a.module_id\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.target_type = 'activity'\n and pr.target_id = a.id\n and pr.org_user_id in (select org_user_id from cohort)\n where a.org_id = ${orgId}\n and a.course_id = ${courseId}\n and coalesce((a.settings ->> 'published')::boolean, true)\n group by a.id, a.settings, m.id, m.title, m.seq, a.seq\n order by m.seq, a.seq\n `);\n const activities: CourseActivityEngagement[] = (\n funnelResult.rows as {\n id: string;\n title: string;\n module_id: string;\n module_title: string;\n started: number;\n completed: number;\n }[]\n ).map((r) => ({\n activityId: r.id,\n title: r.title,\n moduleId: r.module_id,\n moduleTitle: r.module_title,\n started: Number(r.started),\n completed: Number(r.completed),\n }));\n\n return {\n enrolled: Number(summary?.enrolled ?? 0),\n started: Number(summary?.started ?? 0),\n completed: Number(summary?.completed ?? 0),\n avgProgress: Number(summary?.avg_progress ?? 0),\n activities,\n };\n }\n\n // Grants per day for this course over the trailing window, zero-filled so the\n // series has one point per calendar day (UTC) even when nothing was granted.\n async courseEnrollments(\n orgId: string,\n courseId: string,\n days: number,\n ): Promise<CourseEnrollmentPoint[] | null> {\n if (!(await this.courseExists(orgId, courseId))) {\n return null;\n }\n const result = await this.db.execute(sql`\n select\n to_char(d.day, 'YYYY-MM-DD') as date,\n count(${entitlements.id})::int as count\n from generate_series(\n (now() at time zone 'utc')::date - (${days} - 1),\n (now() at time zone 'utc')::date,\n interval '1 day'\n ) as d(day)\n left join ${entitlements}\n on ${entitlements.orgId} = ${orgId}\n and ${entitlements.contentId} = ${courseId}\n and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date\n group by d.day\n order by d.day\n `);\n const rows = result.rows as { date: string; count: number }[];\n return rows.map((r) => ({ date: r.date, count: Number(r.count) }));\n }\n\n async listStudents(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n // Staff share this table now — the role filter is what keeps them out of\n // the students report.\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n\n const rows = await this.db\n .select({\n ...studentProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n // Group by the full composite PK (orgId, id): grouping by id alone gives\n // Postgres no functional dependency for the other students columns.\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.id,\n users.externalId,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .orderBy(...this.resolveOrder(query.sort))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n return {\n rows: rows.map(toStudent),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getStudent(orgId: string, id: string): Promise<Student | null> {\n const [row] = await this.db\n .select({\n ...studentProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)))\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.id,\n users.externalId,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .limit(1);\n return row ? toStudent(row) : null;\n }\n\n // Learner record: one row per course the student holds an effective-active\n // entitlement to (mirroring the course-analytics cohort rule), with progress\n // computed over published activities the same way the course analytics read does.\n async studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null> {\n const [exists] = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)));\n if (!exists) {\n return null;\n }\n\n const result = await this.db.execute(sql`\n with cohort_courses as (\n select distinct c.id, c.title\n from content_access ca\n join courses c on c.org_id = ca.org_id and c.id = ca.content_id\n where ca.org_id = ${orgId}\n and ca.org_user_id = ${id}\n ),\n acts as (\n select a.course_id, a.id\n from activities a\n where a.org_id = ${orgId}\n and a.course_id in (select id from cohort_courses)\n and coalesce((a.settings ->> 'published')::boolean, true)\n )\n select\n cc.id as course_id,\n cc.title,\n count(a.id)::int as total_activities,\n count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,\n min(pr.started_at) as started_at,\n max(pr.updated_at) as last_activity_at,\n (\n select prc.completed_at from progress_records prc\n where prc.org_id = ${orgId}\n and prc.org_user_id = ${id}\n and prc.target_type = 'course'\n and prc.target_id = cc.id\n ) as completed_at\n from cohort_courses cc\n left join acts a on a.course_id = cc.id\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.org_user_id = ${id}\n and pr.target_type = 'activity'\n and pr.target_id = a.id\n group by cc.id, cc.title\n order by cc.title\n `);\n\n const courses: StudentCourseProgress[] = (\n result.rows as {\n course_id: string;\n title: string;\n total_activities: number;\n completed_activities: number;\n started_at: Date | string | null;\n last_activity_at: Date | string | null;\n completed_at: Date | string | null;\n }[]\n ).map((r) => {\n const total = Number(r.total_activities);\n const done = Number(r.completed_activities);\n return {\n courseId: r.course_id,\n title: r.title,\n totalActivities: total,\n completedActivities: done,\n progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,\n startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,\n lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,\n completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null,\n };\n });\n\n const started = courses.filter((c) => c.startedAt !== null).length;\n const completed = courses.filter((c) => c.completedAt !== null).length;\n const avgProgress =\n courses.length > 0\n ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length)\n : 0;\n\n return { enrolled: courses.length, started, completed, avgProgress, courses };\n }\n\n private resolveOrder(sort?: string): SQL[] {\n const descending = sort?.startsWith(\"-\") ?? false;\n const field = sort ? (descending ? sort.slice(1) : sort) : \"firstName\";\n const dir = descending ? desc : asc;\n switch (field) {\n case \"email\":\n return [dir(users.email)];\n case \"entitlementCount\":\n return [dir(entitlementCountExpr)];\n case \"avgProgress\":\n return [dir(avgProgressExpr)];\n case \"joinedAt\":\n return [dir(orgUsers.createdAt)];\n case \"lastActiveAt\":\n return [dir(lastActiveExpr)];\n case \"lastName\":\n return [dir(studentLastName), dir(studentFirstName)];\n case \"firstName\":\n default:\n return [dir(studentFirstName), dir(studentLastName)];\n }\n }\n\n async communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null> {\n const [community] = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, communityId)))\n .limit(1);\n if (!community) {\n return null;\n }\n\n const opens = grantOpens(entitlements, sql`${communityId}`, \"community\");\n const lapsed = and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.status, \"active\"),\n grantExpired(entitlements.expiresAt),\n opens,\n );\n const recent = sql`now() - interval '30 days'`;\n\n const [\n [members],\n [lapsedGrants],\n authors,\n postsPerSpace,\n commentsPerPost,\n [sent],\n [postFlagRow],\n [commentFlagRow],\n [messageFlagRow],\n lapsedHolders,\n ] = await Promise.all([\n this.db\n .select({ count: sql<number>`cast(count(distinct ${contentAccess.orgUserId}) as int)` })\n .from(contentAccess)\n .where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId))),\n this.db\n .select({ count: sql<number>`cast(count(*) as int)` })\n .from(entitlements)\n .where(lapsed),\n union(\n this.db\n .select({ id: posts.orgUserId })\n .from(posts)\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.communityId, communityId),\n sql`${posts.createdAt} >= ${recent}`,\n ),\n ),\n this.db\n .select({ id: comments.orgUserId })\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n eq(comments.communityId, communityId),\n sql`${comments.createdAt} >= ${recent}`,\n ),\n ),\n this.db\n .select({ id: messages.orgUserId })\n .from(messages)\n .where(\n and(\n eq(messages.orgId, orgId),\n eq(messages.communityId, communityId),\n sql`${messages.createdAt} >= ${recent}`,\n ),\n ),\n ),\n this.db\n .select({\n spaceId: spaces.id,\n count: sql<number>`cast(count(${posts.id}) as int)`,\n })\n .from(spaces)\n .leftJoin(\n posts,\n and(\n eq(posts.orgId, spaces.orgId),\n eq(posts.spaceId, spaces.id),\n eq(posts.status, \"published\"),\n ),\n )\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)))\n .groupBy(spaces.id),\n this.db\n .select({\n postId: posts.id,\n count: sql<number>`cast(count(${comments.id}) as int)`,\n })\n .from(posts)\n .leftJoin(\n comments,\n and(\n eq(comments.orgId, posts.orgId),\n eq(comments.postId, posts.id),\n eq(comments.status, \"published\"),\n ),\n )\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.communityId, communityId),\n eq(posts.status, \"published\"),\n ),\n )\n .groupBy(posts.id),\n this.db\n .select({ count: sql<number>`cast(count(*) as int)` })\n .from(messages)\n .where(and(eq(messages.orgId, orgId), eq(messages.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${postFlags.resolvedAt} is not null) as int)`,\n })\n .from(postFlags)\n .innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId)))\n .where(and(eq(postFlags.orgId, orgId), eq(posts.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${commentFlags.resolvedAt} is not null) as int)`,\n })\n .from(commentFlags)\n .innerJoin(\n comments,\n and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId)),\n )\n .where(and(eq(commentFlags.orgId, orgId), eq(comments.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${messageFlags.resolvedAt} is not null) as int)`,\n })\n .from(messageFlags)\n .where(and(eq(messageFlags.orgId, orgId), eq(messageFlags.communityId, communityId))),\n this.db\n .select({ orgUserId: entitlements.orgUserId })\n .from(entitlements)\n .where(lapsed)\n .groupBy(entitlements.orgUserId),\n ]);\n\n const holderIds = lapsedHolders.map((row) => row.orgUserId);\n const [renewed] =\n holderIds.length === 0\n ? [{ count: 0 }]\n : await this.db\n .select({ count: sql<number>`cast(count(distinct ${contentAccess.orgUserId}) as int)` })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.contentId, communityId),\n inArray(contentAccess.orgUserId, holderIds),\n ),\n );\n\n const raised =\n Number(postFlagRow?.raised ?? 0) +\n Number(commentFlagRow?.raised ?? 0) +\n Number(messageFlagRow?.raised ?? 0);\n const resolved =\n Number(postFlagRow?.resolved ?? 0) +\n Number(commentFlagRow?.resolved ?? 0) +\n Number(messageFlagRow?.resolved ?? 0);\n\n return {\n memberCount: Number(members?.count ?? 0),\n activeMembers: authors.length,\n postsPerSpace: postsPerSpace.map((row) => ({\n spaceId: row.spaceId,\n count: Number(row.count),\n })),\n commentsPerPost: commentsPerPost.map((row) => ({\n postId: row.postId,\n count: Number(row.count),\n })),\n messagesSent: Number(sent?.count ?? 0),\n flagsRaised: raised,\n flagsResolved: resolved,\n grantsLapsed: Number(lapsedGrants?.count ?? 0),\n grantsRenewed: Number(renewed?.count ?? 0),\n };\n }\n\n async communityMembers(\n orgId: string,\n communityId: string,\n query: CommunityMembersQuery,\n ): Promise<Page<CommunityMember> | null> {\n const [community] = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, communityId)))\n .limit(1);\n if (!community) {\n return null;\n }\n const holds = exists(\n this.db\n .select({ id: contentAccess.entitlementId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgUsers.orgId),\n eq(contentAccess.orgUserId, orgUsers.id),\n eq(contentAccess.contentId, communityId),\n ),\n ),\n );\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), holds];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n const rows = await this.db\n .select({ ...orgUserProfileColumns, role: orgUsers.role })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n .orderBy(asc(users.firstName), asc(users.lastName), asc(orgUsers.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n return {\n rows: rows.map((row) => ({ ...row, image: row.image ?? null })),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\ntranslateDbErrors(DrizzleAdminRepository);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"admin.js","names":[],"sources":["../../src/repositories/admin.ts"],"sourcesContent":["// reporting/admin — Drizzle repository.\nimport { and, asc, desc, eq, exists, ilike, inArray, or, sql, type SQL } from \"drizzle-orm\";\nimport { union } from \"drizzle-orm/pg-core\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n AdminReportRepository,\n CommunityAnalytics,\n CommunityMembersQuery,\n CourseActivityEngagement,\n CourseAnalytics,\n CourseEnrollmentPoint,\n CourseLearner,\n CourseLearnersQuery,\n EnrollmentPoint,\n EventAnalytics,\n EventAttendeeRow,\n LearnerGrantStatus,\n OverviewStats,\n Page,\n StudentAnalytics,\n StudentCourseProgress,\n StudentItem,\n StudentRow as StudentRecord,\n StudentsQuery,\n} from \"@headless-lms/core/reporting/admin\";\nimport {\n commentFlags,\n comments,\n communities,\n contentAccess,\n courses,\n entitlements,\n invites,\n messageFlags,\n messages,\n orgUsers,\n roleAssignments,\n postFlags,\n posts,\n spaces,\n users,\n} from \"../schema/index.js\";\nimport { user } from \"../schema/better-auth.js\";\nimport type { OrgUserProfile, OrgUserStatus } from \"@headless-lms/core/types\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport {\n orgUserProfileColumns,\n studentFirstName,\n studentLastName,\n studentProfileColumns,\n} from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { grantExpired, grantOpens } from \"./grant-covers.js\";\nimport { eventAnalyticsFigures, eventAttendeePage, upcomingEventCount } from \"./event-reports.js\";\n\nconst entitlementCountExpr = sql<number>`count(${entitlements.id})`;\n// Completion now lives in the progress domain; the students report no longer\n// derives a percentage from entitlements. Placeholder until wired to progress.\nconst avgProgressExpr = sql<number>`0`;\n// Last learning activity: the newest touch on any of the student's progress\n// records. A scalar subquery so it neither fans out the entitlement count nor\n// widens the GROUP BY.\nfunction hasNoOrgRole(): SQL {\n return sql`not exists (\n select 1 from ${roleAssignments} ra\n where ra.org_id = ${orgUsers.orgId} and ra.org_user_id = ${orgUsers.id} and ra.content_id is null\n )`;\n}\n\nfunction invitedAs(roleKey: string): SQL {\n return sql`not exists (\n select 1 from ${invites} i\n where i.org_id = ${orgUsers.orgId} and lower(i.email) = lower(${users.email})\n and i.status = 'pending' and i.role <> ${roleKey}\n )`;\n}\n\nfunction isStudent(memberRoleKey: string): SQL {\n return and(\n hasNoOrgRole(),\n or(\n eq(orgUsers.status, \"active\"),\n and(eq(orgUsers.status, \"invited\"), invitedAs(memberRoleKey)),\n ),\n )!;\n}\n\nconst lastActiveExpr = sql<Date | null>`(\n select max(pr.updated_at) from progress_records pr\n where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}\n)`;\n\ninterface StudentRow {\n id: string;\n email: string;\n image: string | null;\n firstName: string | null;\n lastName: string | null;\n status: OrgUserStatus;\n createdAt: Date;\n entitlementCount: number;\n avgProgress: number;\n lastActiveAt: Date | null;\n}\n\nfunction toStudent(row: StudentRow, items: StudentItem[]): StudentRecord {\n return {\n items,\n id: row.id,\n email: row.email,\n image: row.image ?? null,\n firstName: row.firstName,\n lastName: row.lastName,\n status: row.status,\n entitlementCount: Number(row.entitlementCount),\n avgProgress: Number(row.avgProgress),\n joinedAt: row.createdAt.toISOString(),\n lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null,\n };\n}\n\nexport class DrizzleAdminRepository implements AdminReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async overview(orgId: string): Promise<OverviewStats> {\n const [courseCounts] = await this.db\n .select({\n published: sql<number>`count(*) filter (where ${courses.status} = 'published')`,\n draft: sql<number>`count(*) filter (where ${courses.status} = 'draft')`,\n })\n .from(courses)\n .where(eq(courses.orgId, orgId));\n\n const [entitlementCounts] = await this.db\n .select({\n activeStudents: sql<number>`count(distinct ${contentAccess.orgUserId})`,\n expiringSoon: sql<number>`count(distinct ${contentAccess.entitlementId}) filter (where ${grantExpired(contentAccess.expiresAt, sql`now() + interval '14 days'`)})`,\n })\n .from(contentAccess)\n .where(eq(contentAccess.orgId, orgId));\n\n return {\n publishedCourses: Number(courseCounts?.published ?? 0),\n draftCourses: Number(courseCounts?.draft ?? 0),\n activeStudents: Number(entitlementCounts?.activeStudents ?? 0),\n expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0),\n upcomingEvents: await upcomingEventCount(this.db, orgId),\n };\n }\n\n // Grants per day over the trailing window, zero-filled so the series has one\n // point per calendar day (UTC) even when nothing was granted.\n async enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]> {\n const result = await this.db.execute(sql`\n select\n to_char(d.day, 'YYYY-MM-DD') as date,\n count(${entitlements.id})::int as count\n from generate_series(\n (now() at time zone 'utc')::date - (${days} - 1),\n (now() at time zone 'utc')::date,\n interval '1 day'\n ) as d(day)\n left join ${entitlements}\n on ${entitlements.orgId} = ${orgId}\n and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date\n group by d.day\n order by d.day\n `);\n const rows = result.rows as { date: string; count: number }[];\n return rows.map((r) => ({ date: r.date, count: Number(r.count) }));\n }\n\n private async courseExists(orgId: string, courseId: string): Promise<boolean> {\n const [row] = await this.db\n .select({ id: courses.id })\n .from(courses)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));\n return row !== undefined;\n }\n\n async courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null> {\n if (!(await this.courseExists(orgId, courseId))) {\n return null;\n }\n\n const cohort = sql`\n select distinct org_user_id from content_access\n where org_id = ${orgId}\n and content_id = ${courseId}\n `;\n const publishedActivities = sql`\n select a.id, a.settings, a.seq, a.module_id\n from activities a\n where a.org_id = ${orgId}\n and a.course_id = ${courseId}\n and coalesce((a.settings ->> 'published')::boolean, true)\n `;\n\n const summaryResult = await this.db.execute(sql`\n with cohort as (${cohort}),\n acts as (${publishedActivities}),\n per_learner as (\n select c.org_user_id,\n count(pr.id) filter (where pr.completed_at is not null) as done\n from cohort c\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.org_user_id = c.org_user_id\n and pr.target_type = 'activity'\n and pr.target_id in (select id from acts)\n group by c.org_user_id\n )\n select\n (select count(*) from cohort)::int as enrolled,\n (\n select count(distinct pr.org_user_id) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id in (select org_user_id from cohort)\n and (\n (pr.target_type = 'activity' and pr.target_id in (select id from acts))\n or (pr.target_type = 'course' and pr.target_id = ${courseId})\n )\n )::int as started,\n (\n select count(*) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id in (select org_user_id from cohort)\n and pr.target_type = 'course'\n and pr.target_id = ${courseId}\n and pr.completed_at is not null\n )::int as completed,\n coalesce((\n select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))\n from per_learner\n ), 0)::int as avg_progress\n `);\n const summary = summaryResult.rows[0] as {\n enrolled: number;\n started: number;\n completed: number;\n avg_progress: number;\n };\n\n const funnelResult = await this.db.execute(sql`\n with cohort as (${cohort})\n select\n a.id,\n coalesce(a.settings ->> 'title', '') as title,\n m.id as module_id,\n m.title as module_title,\n count(pr.id)::int as started,\n count(pr.id) filter (where pr.completed_at is not null)::int as completed\n from activities a\n join modules m on m.org_id = a.org_id and m.id = a.module_id\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.target_type = 'activity'\n and pr.target_id = a.id\n and pr.org_user_id in (select org_user_id from cohort)\n where a.org_id = ${orgId}\n and a.course_id = ${courseId}\n and coalesce((a.settings ->> 'published')::boolean, true)\n group by a.id, a.settings, m.id, m.title, m.seq, a.seq\n order by m.seq, a.seq\n `);\n const activities: CourseActivityEngagement[] = (\n funnelResult.rows as {\n id: string;\n title: string;\n module_id: string;\n module_title: string;\n started: number;\n completed: number;\n }[]\n ).map((r) => ({\n activityId: r.id,\n title: r.title,\n moduleId: r.module_id,\n moduleTitle: r.module_title,\n started: Number(r.started),\n completed: Number(r.completed),\n }));\n\n return {\n enrolled: Number(summary?.enrolled ?? 0),\n started: Number(summary?.started ?? 0),\n completed: Number(summary?.completed ?? 0),\n avgProgress: Number(summary?.avg_progress ?? 0),\n activities,\n };\n }\n\n // Grants per day for this course over the trailing window, zero-filled so the\n // series has one point per calendar day (UTC) even when nothing was granted.\n async courseEnrollments(\n orgId: string,\n courseId: string,\n days: number,\n ): Promise<CourseEnrollmentPoint[] | null> {\n if (!(await this.courseExists(orgId, courseId))) {\n return null;\n }\n const result = await this.db.execute(sql`\n select\n to_char(d.day, 'YYYY-MM-DD') as date,\n count(${entitlements.id})::int as count\n from generate_series(\n (now() at time zone 'utc')::date - (${days} - 1),\n (now() at time zone 'utc')::date,\n interval '1 day'\n ) as d(day)\n left join ${entitlements}\n on ${entitlements.orgId} = ${orgId}\n and ${entitlements.contentId} = ${courseId}\n and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date\n group by d.day\n order by d.day\n `);\n const rows = result.rows as { date: string; count: number }[];\n return rows.map((r) => ({ date: r.date, count: Number(r.count) }));\n }\n\n async courseLearners(\n orgId: string,\n courseId: string,\n query: CourseLearnersQuery,\n ): Promise<Page<CourseLearner> | null> {\n if (!(await this.courseExists(orgId, courseId))) {\n return null;\n }\n const expired = grantExpired(entitlements.expiresAt);\n const grantStatus = sql<LearnerGrantStatus>`case\n when ${entitlements.status} = 'revoked' then 'revoked'\n when ${expired} then 'expired'\n else 'active'\n end`;\n const grantRank = sql`case\n when ${entitlements.status} = 'revoked' then 2\n when ${expired} then 1\n else 0\n end`;\n const learnerGrant = this.db.$with(\"learner_grant\").as(\n this.db\n .selectDistinctOn([entitlements.orgUserId], {\n orgUserId: entitlements.orgUserId,\n status: grantStatus.as(\"grant_status\"),\n source: entitlements.source,\n grantedAt: entitlements.grantedAt,\n expiresAt: entitlements.expiresAt,\n })\n .from(entitlements)\n .where(\n and(eq(entitlements.orgId, orgId), grantOpens(entitlements, sql`${courseId}`, \"course\")),\n )\n .orderBy(\n entitlements.orgUserId,\n grantRank,\n sql`${entitlements.expiresAt} desc nulls first`,\n desc(entitlements.grantedAt),\n ),\n );\n const publishedActivities = sql`(\n select a.id from activities a\n where a.org_id = ${orgId}\n and a.course_id = ${courseId}\n and coalesce((a.settings ->> 'published')::boolean, true)\n )`;\n const completedActivities = sql<number>`(\n select count(*) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id = ${orgUsers.id}\n and pr.target_type = 'activity'\n and pr.completed_at is not null\n and pr.target_id in ${publishedActivities}\n )`;\n const lastActivityAt = sql<Date | string | null>`(\n select max(pr.updated_at) from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id = ${orgUsers.id}\n and (\n (pr.target_type = 'activity' and pr.target_id in ${publishedActivities})\n or (pr.target_type = 'course' and pr.target_id = ${courseId})\n )\n )`;\n const completedAt = sql<Date | string | null>`(\n select pr.completed_at from progress_records pr\n where pr.org_id = ${orgId}\n and pr.org_user_id = ${orgUsers.id}\n and pr.target_type = 'course'\n and pr.target_id = ${courseId}\n )`;\n\n const filters: SQL[] = [eq(orgUsers.orgId, orgId)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n const learner = and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, learnerGrant.orgUserId));\n\n const [[totals], [activityCount], rows] = await Promise.all([\n this.db\n .with(learnerGrant)\n .select({ total: sql<number>`count(*)` })\n .from(learnerGrant)\n .innerJoin(orgUsers, learner)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where),\n this.db\n .select({ total: sql<number>`count(*)` })\n .from(sql`${publishedActivities} as published_activities`),\n this.db\n .with(learnerGrant)\n .select({\n ...studentProfileColumns,\n status: learnerGrant.status,\n source: learnerGrant.source,\n grantedAt: learnerGrant.grantedAt,\n expiresAt: learnerGrant.expiresAt,\n completedActivities,\n lastActivityAt,\n completedAt,\n })\n .from(learnerGrant)\n .innerJoin(orgUsers, learner)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n .orderBy(asc(studentFirstName), asc(studentLastName), asc(orgUsers.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize),\n ]);\n\n const totalActivities = Number(activityCount?.total ?? 0);\n return {\n rows: rows.map((row) => {\n const done = Number(row.completedActivities);\n return {\n id: row.id,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email,\n image: row.image ?? null,\n status: row.status,\n source: row.source,\n grantedAt: new Date(row.grantedAt).toISOString(),\n expiresAt: row.expiresAt ? new Date(row.expiresAt).toISOString() : null,\n totalActivities,\n completedActivities: done,\n progress: totalActivities > 0 ? Math.round(Math.min(done / totalActivities, 1) * 100) : 0,\n lastActivityAt: row.lastActivityAt ? new Date(row.lastActivityAt).toISOString() : null,\n completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : null,\n };\n }),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async listStudents(\n orgId: string,\n query: StudentsQuery,\n memberRoleKey: string,\n ): Promise<Page<StudentRecord>> {\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), isStudent(memberRoleKey)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n\n const rows = await this.db\n .select({\n ...studentProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n // Group by the full composite PK (orgId, id): grouping by id alone gives\n // Postgres no functional dependency for the other students columns.\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.id,\n users.externalId,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .orderBy(...this.resolveOrder(query.sort))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const items = await this.itemsOf(\n orgId,\n rows.map((row) => row.id),\n );\n return {\n rows: rows.map((row) => toStudent(row, items.get(row.id) ?? [])),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getStudent(orgId: string, id: string): Promise<StudentRecord | null> {\n const [row] = await this.db\n .select({\n ...studentProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n lastActiveAt: lastActiveExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.id,\n users.externalId,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .limit(1);\n if (!row) {\n return null;\n }\n const items = await this.itemsOf(orgId, [row.id]);\n return toStudent(row, items.get(row.id) ?? []);\n }\n\n private async itemsOf(orgId: string, orgUserIds: string[]): Promise<Map<string, StudentItem[]>> {\n const byPerson = new Map<string, StudentItem[]>();\n if (orgUserIds.length === 0) {\n return byPerson;\n }\n const rows = await this.db\n .select({\n orgUserId: roleAssignments.orgUserId,\n contentId: roleAssignments.contentId,\n contentType: roleAssignments.contentType,\n })\n .from(roleAssignments)\n .where(\n and(\n eq(roleAssignments.orgId, orgId),\n inArray(roleAssignments.orgUserId, orgUserIds),\n sql`${roleAssignments.contentId} is not null`,\n ),\n )\n .orderBy(asc(roleAssignments.createdAt));\n for (const row of rows) {\n if (row.contentId === null || row.contentType === null) {\n continue;\n }\n const items = byPerson.get(row.orgUserId) ?? [];\n items.push({ contentId: row.contentId, contentType: row.contentType });\n byPerson.set(row.orgUserId, items);\n }\n return byPerson;\n }\n\n // Learner record: one row per course the student holds an effective-active\n // entitlement to (mirroring the course-analytics cohort rule), with progress\n // computed over published activities the same way the course analytics read does.\n async studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null> {\n const [exists] = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)));\n if (!exists) {\n return null;\n }\n\n const result = await this.db.execute(sql`\n with cohort_courses as (\n select distinct c.id, c.title\n from content_access ca\n join courses c on c.org_id = ca.org_id and c.id = ca.content_id\n where ca.org_id = ${orgId}\n and ca.org_user_id = ${id}\n ),\n acts as (\n select a.course_id, a.id\n from activities a\n where a.org_id = ${orgId}\n and a.course_id in (select id from cohort_courses)\n and coalesce((a.settings ->> 'published')::boolean, true)\n )\n select\n cc.id as course_id,\n cc.title,\n count(a.id)::int as total_activities,\n count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,\n min(pr.started_at) as started_at,\n max(pr.updated_at) as last_activity_at,\n (\n select prc.completed_at from progress_records prc\n where prc.org_id = ${orgId}\n and prc.org_user_id = ${id}\n and prc.target_type = 'course'\n and prc.target_id = cc.id\n ) as completed_at\n from cohort_courses cc\n left join acts a on a.course_id = cc.id\n left join progress_records pr\n on pr.org_id = ${orgId}\n and pr.org_user_id = ${id}\n and pr.target_type = 'activity'\n and pr.target_id = a.id\n group by cc.id, cc.title\n order by cc.title\n `);\n\n const courses: StudentCourseProgress[] = (\n result.rows as {\n course_id: string;\n title: string;\n total_activities: number;\n completed_activities: number;\n started_at: Date | string | null;\n last_activity_at: Date | string | null;\n completed_at: Date | string | null;\n }[]\n ).map((r) => {\n const total = Number(r.total_activities);\n const done = Number(r.completed_activities);\n return {\n courseId: r.course_id,\n title: r.title,\n totalActivities: total,\n completedActivities: done,\n progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,\n startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,\n lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,\n completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null,\n };\n });\n\n const started = courses.filter((c) => c.startedAt !== null).length;\n const completed = courses.filter((c) => c.completedAt !== null).length;\n const avgProgress =\n courses.length > 0\n ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length)\n : 0;\n\n return { enrolled: courses.length, started, completed, avgProgress, courses };\n }\n\n private resolveOrder(sort?: string): SQL[] {\n const descending = sort?.startsWith(\"-\") ?? false;\n const field = sort ? (descending ? sort.slice(1) : sort) : \"firstName\";\n const dir = descending ? desc : asc;\n switch (field) {\n case \"email\":\n return [dir(users.email)];\n case \"entitlementCount\":\n return [dir(entitlementCountExpr)];\n case \"avgProgress\":\n return [dir(avgProgressExpr)];\n case \"joinedAt\":\n return [dir(orgUsers.createdAt)];\n case \"lastActiveAt\":\n return [dir(lastActiveExpr)];\n case \"lastName\":\n return [dir(studentLastName), dir(studentFirstName)];\n case \"firstName\":\n default:\n return [dir(studentFirstName), dir(studentLastName)];\n }\n }\n\n async communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null> {\n const [community] = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, communityId)))\n .limit(1);\n if (!community) {\n return null;\n }\n\n const opens = grantOpens(entitlements, sql`${communityId}`, \"community\");\n const lapsed = and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.status, \"active\"),\n grantExpired(entitlements.expiresAt),\n opens,\n );\n const recent = sql`now() - interval '30 days'`;\n\n const [\n [members],\n [lapsedGrants],\n authors,\n postsPerSpace,\n commentsPerPost,\n [sent],\n [postFlagRow],\n [commentFlagRow],\n [messageFlagRow],\n lapsedHolders,\n ] = await Promise.all([\n this.db\n .select({ count: sql<number>`cast(count(distinct ${contentAccess.orgUserId}) as int)` })\n .from(contentAccess)\n .where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId))),\n this.db\n .select({ count: sql<number>`cast(count(*) as int)` })\n .from(entitlements)\n .where(lapsed),\n union(\n this.db\n .select({ id: posts.orgUserId })\n .from(posts)\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.communityId, communityId),\n sql`${posts.createdAt} >= ${recent}`,\n ),\n ),\n this.db\n .select({ id: comments.orgUserId })\n .from(comments)\n .where(\n and(\n eq(comments.orgId, orgId),\n eq(comments.communityId, communityId),\n sql`${comments.createdAt} >= ${recent}`,\n ),\n ),\n this.db\n .select({ id: messages.orgUserId })\n .from(messages)\n .where(\n and(\n eq(messages.orgId, orgId),\n eq(messages.communityId, communityId),\n sql`${messages.createdAt} >= ${recent}`,\n ),\n ),\n ),\n this.db\n .select({\n spaceId: spaces.id,\n count: sql<number>`cast(count(${posts.id}) as int)`,\n })\n .from(spaces)\n .leftJoin(\n posts,\n and(\n eq(posts.orgId, spaces.orgId),\n eq(posts.spaceId, spaces.id),\n eq(posts.status, \"published\"),\n ),\n )\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)))\n .groupBy(spaces.id),\n this.db\n .select({\n postId: posts.id,\n count: sql<number>`cast(count(${comments.id}) as int)`,\n })\n .from(posts)\n .leftJoin(\n comments,\n and(\n eq(comments.orgId, posts.orgId),\n eq(comments.postId, posts.id),\n eq(comments.status, \"published\"),\n ),\n )\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.communityId, communityId),\n eq(posts.status, \"published\"),\n ),\n )\n .groupBy(posts.id),\n this.db\n .select({ count: sql<number>`cast(count(*) as int)` })\n .from(messages)\n .where(and(eq(messages.orgId, orgId), eq(messages.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${postFlags.resolvedAt} is not null) as int)`,\n })\n .from(postFlags)\n .innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId)))\n .where(and(eq(postFlags.orgId, orgId), eq(posts.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${commentFlags.resolvedAt} is not null) as int)`,\n })\n .from(commentFlags)\n .innerJoin(\n comments,\n and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId)),\n )\n .where(and(eq(commentFlags.orgId, orgId), eq(comments.communityId, communityId))),\n this.db\n .select({\n raised: sql<number>`cast(count(*) as int)`,\n resolved: sql<number>`cast(count(*) filter (where ${messageFlags.resolvedAt} is not null) as int)`,\n })\n .from(messageFlags)\n .where(and(eq(messageFlags.orgId, orgId), eq(messageFlags.communityId, communityId))),\n this.db\n .select({ orgUserId: entitlements.orgUserId })\n .from(entitlements)\n .where(lapsed)\n .groupBy(entitlements.orgUserId),\n ]);\n\n const holderIds = lapsedHolders.map((row) => row.orgUserId);\n const [renewed] =\n holderIds.length === 0\n ? [{ count: 0 }]\n : await this.db\n .select({ count: sql<number>`cast(count(distinct ${contentAccess.orgUserId}) as int)` })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.contentId, communityId),\n inArray(contentAccess.orgUserId, holderIds),\n ),\n );\n\n const raised =\n Number(postFlagRow?.raised ?? 0) +\n Number(commentFlagRow?.raised ?? 0) +\n Number(messageFlagRow?.raised ?? 0);\n const resolved =\n Number(postFlagRow?.resolved ?? 0) +\n Number(commentFlagRow?.resolved ?? 0) +\n Number(messageFlagRow?.resolved ?? 0);\n\n return {\n memberCount: Number(members?.count ?? 0),\n activeMembers: authors.length,\n postsPerSpace: postsPerSpace.map((row) => ({\n spaceId: row.spaceId,\n count: Number(row.count),\n })),\n commentsPerPost: commentsPerPost.map((row) => ({\n postId: row.postId,\n count: Number(row.count),\n })),\n messagesSent: Number(sent?.count ?? 0),\n flagsRaised: raised,\n flagsResolved: resolved,\n grantsLapsed: Number(lapsedGrants?.count ?? 0),\n grantsRenewed: Number(renewed?.count ?? 0),\n };\n }\n\n async communityMembers(\n orgId: string,\n communityId: string,\n query: CommunityMembersQuery,\n ): Promise<Page<OrgUserProfile> | null> {\n const [community] = await this.db\n .select({ id: communities.id })\n .from(communities)\n .where(and(eq(communities.orgId, orgId), eq(communities.id, communityId)))\n .limit(1);\n if (!community) {\n return null;\n }\n const holds = exists(\n this.db\n .select({ id: contentAccess.entitlementId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgUsers.orgId),\n eq(contentAccess.orgUserId, orgUsers.id),\n eq(contentAccess.contentId, communityId),\n ),\n ),\n );\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), holds];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like))!,\n );\n }\n const where = and(...filters);\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n const rows = await this.db\n .select(orgUserProfileColumns)\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n .orderBy(asc(users.firstName), asc(users.lastName), asc(orgUsers.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n return {\n rows: rows.map((row) => ({ ...row, image: row.image ?? null })),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\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 eventAnalytics(\n orgId: string,\n eventId: string,\n startsAt: Date,\n ): Promise<Omit<EventAnalytics, \"capacity\">> {\n return eventAnalyticsFigures(this.db, orgId, eventId, startsAt);\n }\n}\ntranslateDbErrors(DrizzleAdminRepository);\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DbExecutor } from "../client.js";
|
|
2
|
-
import { Asset, AssetsQuery, Logger, Page } from "@headless-lms/core/types";
|
|
2
|
+
import { Asset, AssetsQuery, ContentType, Logger, Page } from "@headless-lms/core/types";
|
|
3
3
|
import { AssetsRepository } from "@headless-lms/core/assets";
|
|
4
4
|
//#region src/repositories/assets.d.ts
|
|
5
5
|
declare class DrizzleAssetsRepository implements AssetsRepository {
|
|
@@ -14,6 +14,7 @@ declare class DrizzleAssetsRepository implements AssetsRepository {
|
|
|
14
14
|
delete(orgId: string, id: string): Promise<boolean>;
|
|
15
15
|
listUnreferencedMemberUploads(orgId: string, before: Date): Promise<Asset[]>;
|
|
16
16
|
heldKeys(orgId: string, keys: string[]): Promise<string[]>;
|
|
17
|
+
contentTypeOf(orgId: string, contentId: string): Promise<ContentType | null>;
|
|
17
18
|
}
|
|
18
19
|
//#endregion
|
|
19
20
|
export { DrizzleAssetsRepository };
|