@headless-lms/adapter-db 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/index.d.ts +7 -6
  2. package/dist/index.js +7 -6
  3. package/dist/repositories/admin.d.ts +23 -0
  4. package/dist/repositories/admin.js +393 -0
  5. package/dist/repositories/admin.js.map +1 -0
  6. package/dist/repositories/assets.d.ts +6 -2
  7. package/dist/repositories/assets.js +50 -9
  8. package/dist/repositories/assets.js.map +1 -1
  9. package/dist/repositories/automations.d.ts +6 -2
  10. package/dist/repositories/automations.js +45 -2
  11. package/dist/repositories/automations.js.map +1 -1
  12. package/dist/repositories/chats.d.ts +55 -0
  13. package/dist/repositories/chats.js +454 -0
  14. package/dist/repositories/chats.js.map +1 -0
  15. package/dist/repositories/content.d.ts +24 -1
  16. package/dist/repositories/content.js +293 -9
  17. package/dist/repositories/content.js.map +1 -1
  18. package/dist/repositories/discussion.d.ts +10 -3
  19. package/dist/repositories/discussion.js +70 -15
  20. package/dist/repositories/discussion.js.map +1 -1
  21. package/dist/repositories/entitlements.d.ts +29 -9
  22. package/dist/repositories/entitlements.js +207 -17
  23. package/dist/repositories/entitlements.js.map +1 -1
  24. package/dist/repositories/grant-covers.d.ts +20 -0
  25. package/dist/repositories/grant-covers.js +55 -0
  26. package/dist/repositories/grant-covers.js.map +1 -0
  27. package/dist/repositories/identity.d.ts +4 -2
  28. package/dist/repositories/identity.js +32 -1
  29. package/dist/repositories/identity.js.map +1 -1
  30. package/dist/repositories/member.d.ts +16 -0
  31. package/dist/repositories/member.js +63 -0
  32. package/dist/repositories/member.js.map +1 -0
  33. package/dist/repositories/members.js +2 -2
  34. package/dist/repositories/members.js.map +1 -1
  35. package/dist/repositories/notifications.d.ts +23 -0
  36. package/dist/repositories/notifications.js +70 -0
  37. package/dist/repositories/notifications.js.map +1 -0
  38. package/dist/repositories/org-user-profile.d.ts +214 -1
  39. package/dist/repositories/org-user-profile.js +23 -2
  40. package/dist/repositories/org-user-profile.js.map +1 -1
  41. package/dist/repositories/organizations.d.ts +3 -2
  42. package/dist/repositories/organizations.js +20 -8
  43. package/dist/repositories/organizations.js.map +1 -1
  44. package/dist/repositories/pg-errors.js +3 -1
  45. package/dist/repositories/pg-errors.js.map +1 -1
  46. package/dist/repositories/posts.d.ts +39 -0
  47. package/dist/repositories/posts.js +255 -0
  48. package/dist/repositories/posts.js.map +1 -0
  49. package/dist/schema/assets.d.ts +19 -2
  50. package/dist/schema/assets.js +10 -4
  51. package/dist/schema/assets.js.map +1 -1
  52. package/dist/schema/automations.d.ts +38 -0
  53. package/dist/schema/automations.js +5 -1
  54. package/dist/schema/automations.js.map +1 -1
  55. package/dist/schema/better-auth.d.ts +1613 -238
  56. package/dist/schema/better-auth.js +107 -22
  57. package/dist/schema/better-auth.js.map +1 -1
  58. package/dist/schema/content.d.ts +642 -5
  59. package/dist/schema/content.js +119 -10
  60. package/dist/schema/content.js.map +1 -1
  61. package/dist/schema/discussion.d.ts +2045 -55
  62. package/dist/schema/discussion.js +426 -11
  63. package/dist/schema/discussion.js.map +1 -1
  64. package/dist/schema/entitlements.d.ts +432 -1
  65. package/dist/schema/entitlements.js +109 -4
  66. package/dist/schema/entitlements.js.map +1 -1
  67. package/dist/schema/identity.d.ts +517 -1
  68. package/dist/schema/identity.js +118 -2
  69. package/dist/schema/identity.js.map +1 -1
  70. package/dist/schema/index.d.ts +6 -6
  71. package/dist/schema/index.js +30 -7
  72. package/dist/schema/organizations.d.ts +4 -4
  73. package/dist/schema/organizations.js +2 -0
  74. package/dist/schema/organizations.js.map +1 -1
  75. package/drizzle/0003_community.sql +340 -0
  76. package/drizzle/0004_assets_sources.sql +1 -0
  77. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  78. package/drizzle/0006_community_review_fixes.sql +24 -0
  79. package/drizzle/0007_notifications.sql +37 -0
  80. package/drizzle/0008_content_access.sql +13 -0
  81. package/drizzle/0009_community_indexes.sql +56 -0
  82. package/drizzle/0010_mcp_access.sql +114 -0
  83. package/drizzle/meta/0003_snapshot.json +7061 -0
  84. package/drizzle/meta/0004_snapshot.json +7068 -0
  85. package/drizzle/meta/0005_snapshot.json +7180 -0
  86. package/drizzle/meta/0006_snapshot.json +7298 -0
  87. package/drizzle/meta/0007_snapshot.json +7620 -0
  88. package/drizzle/meta/0008_snapshot.json +7666 -0
  89. package/drizzle/meta/0009_snapshot.json +8185 -0
  90. package/drizzle/meta/0010_snapshot.json +8813 -0
  91. package/drizzle/meta/_journal.json +57 -1
  92. package/package.json +2 -2
  93. package/dist/repositories/course-analytics.d.ts +0 -15
  94. package/dist/repositories/course-analytics.js +0 -137
  95. package/dist/repositories/course-analytics.js.map +0 -1
  96. package/dist/repositories/dashboard.d.ts +0 -14
  97. package/dist/repositories/dashboard.js +0 -57
  98. package/dist/repositories/dashboard.js.map +0 -1
  99. package/dist/repositories/learn.d.ts +0 -19
  100. package/dist/repositories/learn.js +0 -56
  101. package/dist/repositories/learn.js.map +0 -1
  102. package/dist/repositories/students.d.ts +0 -16
  103. package/dist/repositories/students.js +0 -160
  104. package/dist/repositories/students.js.map +0 -1
@@ -22,6 +22,62 @@
22
22
  "when": 1790675689629,
23
23
  "tag": "0002_automation_kinds",
24
24
  "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1792574273608,
30
+ "tag": "0003_community",
31
+ "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1792652068527,
37
+ "tag": "0004_assets_sources",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "7",
43
+ "when": 1792654657973,
44
+ "tag": "0005_notices_and_seed_index",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1792664309207,
51
+ "tag": "0006_community_review_fixes",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "7",
57
+ "when": 1793025210130,
58
+ "tag": "0007_notifications",
59
+ "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "7",
64
+ "when": 1793267692172,
65
+ "tag": "0008_content_access",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "7",
71
+ "when": 1793271619441,
72
+ "tag": "0009_community_indexes",
73
+ "breakpoints": true
74
+ },
75
+ {
76
+ "idx": 10,
77
+ "version": "7",
78
+ "when": 1793328918383,
79
+ "tag": "0010_mcp_access",
80
+ "breakpoints": true
25
81
  }
26
82
  ]
27
- }
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-lms/adapter-db",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "drizzle-orm": "^0.45.2",
23
23
  "pg": "^8.13.1",
24
- "@headless-lms/core": "0.2.0"
24
+ "@headless-lms/core": "0.3.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.10.2",
@@ -1,15 +0,0 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Logger } from "@headless-lms/core/shared/ports";
3
- import { CourseAnalytics, CourseEnrollmentPoint, CoursesReportRepository } from "@headless-lms/core/reporting/courses";
4
- //#region src/repositories/course-analytics.d.ts
5
- declare class DrizzleCourseAnalyticsRepository implements CoursesReportRepository {
6
- private readonly db;
7
- private readonly logger;
8
- constructor(db: NodePgDatabase, logger?: Logger);
9
- private courseExists;
10
- analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
11
- enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
12
- }
13
- //#endregion
14
- export { DrizzleCourseAnalyticsRepository };
15
- //# sourceMappingURL=course-analytics.d.ts.map
@@ -1,137 +0,0 @@
1
- import { courses } from "../schema/content.js";
2
- import { entitlements } from "../schema/entitlements.js";
3
- import "../schema/index.js";
4
- import { translateDbErrors } from "./pg-errors.js";
5
- import { and, eq, sql } from "drizzle-orm";
6
- import { noopLogger } from "@headless-lms/core/shared/logger";
7
- //#region src/repositories/course-analytics.ts
8
- var DrizzleCourseAnalyticsRepository = class {
9
- db;
10
- logger;
11
- constructor(db, logger = noopLogger) {
12
- this.db = db;
13
- this.logger = logger;
14
- }
15
- async courseExists(orgId, courseId) {
16
- const [row] = await this.db.select({ id: courses.id }).from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));
17
- return row !== void 0;
18
- }
19
- async analytics(orgId, courseId) {
20
- if (!await this.courseExists(orgId, courseId)) return null;
21
- const cohort = sql`
22
- select org_user_id from entitlements
23
- where org_id = ${orgId}
24
- and content_id = ${courseId}
25
- and status = 'active'
26
- and (expires_at is null or expires_at >= now())
27
- `;
28
- const publishedActivities = sql`
29
- select a.id, a.settings, a.seq, a.module_id
30
- from activities a
31
- where a.org_id = ${orgId}
32
- and a.course_id = ${courseId}
33
- and coalesce((a.settings ->> 'published')::boolean, true)
34
- `;
35
- const summary = (await this.db.execute(sql`
36
- with cohort as (${cohort}),
37
- acts as (${publishedActivities}),
38
- per_learner as (
39
- select c.org_user_id,
40
- count(pr.id) filter (where pr.completed_at is not null) as done
41
- from cohort c
42
- left join progress_records pr
43
- on pr.org_id = ${orgId}
44
- and pr.org_user_id = c.org_user_id
45
- and pr.target_type = 'activity'
46
- and pr.target_id in (select id from acts)
47
- group by c.org_user_id
48
- )
49
- select
50
- (select count(*) from cohort)::int as enrolled,
51
- (
52
- select count(distinct pr.org_user_id) from progress_records pr
53
- where pr.org_id = ${orgId}
54
- and pr.org_user_id in (select org_user_id from cohort)
55
- and (
56
- (pr.target_type = 'activity' and pr.target_id in (select id from acts))
57
- or (pr.target_type = 'course' and pr.target_id = ${courseId})
58
- )
59
- )::int as started,
60
- (
61
- select count(*) from progress_records pr
62
- where pr.org_id = ${orgId}
63
- and pr.org_user_id in (select org_user_id from cohort)
64
- and pr.target_type = 'course'
65
- and pr.target_id = ${courseId}
66
- and pr.completed_at is not null
67
- )::int as completed,
68
- coalesce((
69
- select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))
70
- from per_learner
71
- ), 0)::int as avg_progress
72
- `)).rows[0];
73
- const activities = (await this.db.execute(sql`
74
- with cohort as (${cohort})
75
- select
76
- a.id,
77
- coalesce(a.settings ->> 'title', '') as title,
78
- m.id as module_id,
79
- m.title as module_title,
80
- count(pr.id)::int as started,
81
- count(pr.id) filter (where pr.completed_at is not null)::int as completed
82
- from activities a
83
- join modules m on m.org_id = a.org_id and m.id = a.module_id
84
- left join progress_records pr
85
- on pr.org_id = ${orgId}
86
- and pr.target_type = 'activity'
87
- and pr.target_id = a.id
88
- and pr.org_user_id in (select org_user_id from cohort)
89
- where a.org_id = ${orgId}
90
- and a.course_id = ${courseId}
91
- and coalesce((a.settings ->> 'published')::boolean, true)
92
- group by a.id, a.settings, m.id, m.title, m.seq, a.seq
93
- order by m.seq, a.seq
94
- `)).rows.map((r) => ({
95
- activityId: r.id,
96
- title: r.title,
97
- moduleId: r.module_id,
98
- moduleTitle: r.module_title,
99
- started: Number(r.started),
100
- completed: Number(r.completed)
101
- }));
102
- return {
103
- enrolled: Number(summary?.enrolled ?? 0),
104
- started: Number(summary?.started ?? 0),
105
- completed: Number(summary?.completed ?? 0),
106
- avgProgress: Number(summary?.avg_progress ?? 0),
107
- activities
108
- };
109
- }
110
- async enrollments(orgId, courseId, days) {
111
- if (!await this.courseExists(orgId, courseId)) return null;
112
- return (await this.db.execute(sql`
113
- select
114
- to_char(d.day, 'YYYY-MM-DD') as date,
115
- count(${entitlements.id})::int as count
116
- from generate_series(
117
- (now() at time zone 'utc')::date - (${days} - 1),
118
- (now() at time zone 'utc')::date,
119
- interval '1 day'
120
- ) as d(day)
121
- left join ${entitlements}
122
- on ${entitlements.orgId} = ${orgId}
123
- and ${entitlements.contentId} = ${courseId}
124
- and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
125
- group by d.day
126
- order by d.day
127
- `)).rows.map((r) => ({
128
- date: r.date,
129
- count: Number(r.count)
130
- }));
131
- }
132
- };
133
- translateDbErrors(DrizzleCourseAnalyticsRepository);
134
- //#endregion
135
- export { DrizzleCourseAnalyticsRepository };
136
-
137
- //# sourceMappingURL=course-analytics.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"course-analytics.js","names":[],"sources":["../../src/repositories/course-analytics.ts"],"sourcesContent":["// reporting/courses — Drizzle repository (implements the core outbound port).\n// Per-course engagement figures. The cohort is every org user holding an\n// effective-active entitlement to the course (status='active' and not expired);\n// completions by revoked/expired students are deliberately excluded so the\n// numbers describe the current roster. An activity counts toward the\n// denominator when its settings blob does not say published=false (missing ⇒\n// published), mirroring reporting/learn.\nimport { and, eq, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n CourseAnalytics,\n CourseActivityEngagement,\n CourseEnrollmentPoint,\n CoursesReportRepository,\n} from \"@headless-lms/core/reporting/courses\";\nimport { courses, entitlements } 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\";\n\nexport class DrizzleCourseAnalyticsRepository implements CoursesReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\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 analytics(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 org_user_id from entitlements\n where org_id = ${orgId}\n and content_id = ${courseId}\n and status = 'active'\n and (expires_at is null or expires_at >= now())\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 enrollments(\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}\ntranslateDbErrors(DrizzleCourseAnalyticsRepository);\n"],"mappings":";;;;;;;AAoBA,IAAa,mCAAb,MAAiF;CAE5D;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAc,aAAa,OAAe,UAAoC;EAC5E,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,EAAE,IAAI,QAAQ,GAAG,CAAC,CAAC,CAC1B,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC;EAChE,OAAO,QAAQ,KAAA;CACjB;CAEA,MAAM,UAAU,OAAe,UAAmD;EAChF,IAAI,CAAE,MAAM,KAAK,aAAa,OAAO,QAAQ,GAC3C,OAAO;EAGT,MAAM,SAAS,GAAG;;uBAEC,MAAM;2BACF,SAAS;;;;EAIhC,MAAM,sBAAsB,GAAG;;;yBAGV,MAAM;4BACH,SAAS;;;EA0CjC,MAAM,WAAU,MAtCY,KAAK,GAAG,QAAQ,GAAG;wBAC3B,OAAO;iBACd,oBAAoB;;;;;;2BAMV,MAAM;;;;;;;;;;8BAUH,MAAM;;;;iEAI6B,SAAS;;;;;8BAK5C,MAAM;;;iCAGH,SAAS;;;;;;;KAOrC,EAAA,CAC6B,KAAK;EA6BnC,MAAM,cACJ,MAvByB,KAAK,GAAG,QAAQ,GAAG;wBAC1B,OAAO;;;;;;;;;;;yBAWN,MAAM;;;;yBAIN,MAAM;4BACH,SAAS;;;;KAIhC,EAAA,CAEc,KAQb,KAAK,OAAO;GACZ,YAAY,EAAE;GACd,OAAO,EAAE;GACT,UAAU,EAAE;GACZ,aAAa,EAAE;GACf,SAAS,OAAO,EAAE,OAAO;GACzB,WAAW,OAAO,EAAE,SAAS;EAC/B,EAAE;EAEF,OAAO;GACL,UAAU,OAAO,SAAS,YAAY,CAAC;GACvC,SAAS,OAAO,SAAS,WAAW,CAAC;GACrC,WAAW,OAAO,SAAS,aAAa,CAAC;GACzC,aAAa,OAAO,SAAS,gBAAgB,CAAC;GAC9C;EACF;CACF;CAIA,MAAM,YACJ,OACA,UACA,MACyC;EACzC,IAAI,CAAE,MAAM,KAAK,aAAa,OAAO,QAAQ,GAC3C,OAAO;EAmBT,QADa,MAhBQ,KAAK,GAAG,QAAQ,GAAG;;;gBAG5B,aAAa,GAAG;;8CAEc,KAAK;;;;kBAIjC,aAAa;aAClB,aAAa,MAAM,KAAK,MAAM;cAC7B,aAAa,UAAU,KAAK,SAAS;eACpC,aAAa,UAAU;;;KAGjC,EAAA,CACmB,KACR,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,OAAO,OAAO,EAAE,KAAK;EAAE,EAAE;CACnE;AACF;AACA,kBAAkB,gCAAgC"}
@@ -1,14 +0,0 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Logger } from "@headless-lms/core/shared/ports";
3
- import { DashboardReportRepository, EnrollmentPoint, OverviewStats } from "@headless-lms/core/reporting/dashboard";
4
- //#region src/repositories/dashboard.d.ts
5
- declare class DrizzleDashboardRepository implements DashboardReportRepository {
6
- private readonly db;
7
- private readonly logger;
8
- constructor(db: NodePgDatabase, logger?: Logger);
9
- overview(orgId: string): Promise<OverviewStats>;
10
- enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]>;
11
- }
12
- //#endregion
13
- export { DrizzleDashboardRepository };
14
- //# sourceMappingURL=dashboard.d.ts.map
@@ -1,57 +0,0 @@
1
- import { courses } from "../schema/content.js";
2
- import { entitlements } from "../schema/entitlements.js";
3
- import "../schema/index.js";
4
- import { translateDbErrors } from "./pg-errors.js";
5
- import { and, eq, gte, isNull, or, sql } from "drizzle-orm";
6
- import { noopLogger } from "@headless-lms/core/shared/logger";
7
- //#region src/repositories/dashboard.ts
8
- var DrizzleDashboardRepository = class {
9
- db;
10
- logger;
11
- constructor(db, logger = noopLogger) {
12
- this.db = db;
13
- this.logger = logger;
14
- }
15
- async overview(orgId) {
16
- const [courseCounts] = await this.db.select({
17
- published: sql`count(*) filter (where ${courses.status} = 'published')`,
18
- draft: sql`count(*) filter (where ${courses.status} = 'draft')`
19
- }).from(courses).where(eq(courses.orgId, orgId));
20
- const effectiveActive = and(eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), or(isNull(entitlements.expiresAt), gte(entitlements.expiresAt, sql`now()`)));
21
- const [entitlementCounts] = await this.db.select({
22
- activeStudents: sql`count(distinct ${entitlements.orgUserId})`,
23
- expiringSoon: sql`count(*) filter (where ${entitlements.expiresAt} is not null and ${entitlements.expiresAt} < now() + interval '14 days')`
24
- }).from(entitlements).where(effectiveActive);
25
- return {
26
- publishedCourses: Number(courseCounts?.published ?? 0),
27
- draftCourses: Number(courseCounts?.draft ?? 0),
28
- activeStudents: Number(entitlementCounts?.activeStudents ?? 0),
29
- expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0)
30
- };
31
- }
32
- async enrollments(orgId, days) {
33
- return (await this.db.execute(sql`
34
- select
35
- to_char(d.day, 'YYYY-MM-DD') as date,
36
- count(${entitlements.id})::int as count
37
- from generate_series(
38
- (now() at time zone 'utc')::date - (${days} - 1),
39
- (now() at time zone 'utc')::date,
40
- interval '1 day'
41
- ) as d(day)
42
- left join ${entitlements}
43
- on ${entitlements.orgId} = ${orgId}
44
- and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
45
- group by d.day
46
- order by d.day
47
- `)).rows.map((r) => ({
48
- date: r.date,
49
- count: Number(r.count)
50
- }));
51
- }
52
- };
53
- translateDbErrors(DrizzleDashboardRepository);
54
- //#endregion
55
- export { DrizzleDashboardRepository };
56
-
57
- //# sourceMappingURL=dashboard.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dashboard.js","names":[],"sources":["../../src/repositories/dashboard.ts"],"sourcesContent":["// dashboard — Drizzle repository (implements the core outbound port).\n// Back-office overview counts, every figure scoped to the active org. An\n// entitlement is \"effective-active\" when status='active' and it has not expired\n// (expires_at null or in the future) — expiry is derived at read time.\nimport { and, eq, gte, isNull, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DashboardReportRepository,\n EnrollmentPoint,\n OverviewStats,\n} from \"@headless-lms/core/reporting/dashboard\";\nimport { courses, entitlements } 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\";\n\nexport class DrizzleDashboardRepository implements DashboardReportRepository {\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 effectiveActive = and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.status, \"active\"),\n or(isNull(entitlements.expiresAt), gte(entitlements.expiresAt, sql`now()`)),\n );\n\n const [entitlementCounts] = await this.db\n .select({\n activeStudents: sql<number>`count(distinct ${entitlements.orgUserId})`,\n expiringSoon: sql<number>`count(*) filter (where ${entitlements.expiresAt} is not null and ${entitlements.expiresAt} < now() + interval '14 days')`,\n })\n .from(entitlements)\n .where(effectiveActive);\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}\ntranslateDbErrors(DrizzleDashboardRepository);\n"],"mappings":";;;;;;;AAgBA,IAAa,6BAAb,MAA6E;CAExD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,SAAS,OAAuC;EACpD,MAAM,CAAC,gBAAgB,MAAM,KAAK,GAC/B,OAAO;GACN,WAAW,GAAW,0BAA0B,QAAQ,OAAO;GAC/D,OAAO,GAAW,0BAA0B,QAAQ,OAAO;EAC7D,CAAC,CAAC,CACD,KAAK,OAAO,CAAC,CACb,MAAM,GAAG,QAAQ,OAAO,KAAK,CAAC;EAEjC,MAAM,kBAAkB,IACtB,GAAG,aAAa,OAAO,KAAK,GAC5B,GAAG,aAAa,QAAQ,QAAQ,GAChC,GAAG,OAAO,aAAa,SAAS,GAAG,IAAI,aAAa,WAAW,GAAG,OAAO,CAAC,CAC5E;EAEA,MAAM,CAAC,qBAAqB,MAAM,KAAK,GACpC,OAAO;GACN,gBAAgB,GAAW,kBAAkB,aAAa,UAAU;GACpE,cAAc,GAAW,0BAA0B,aAAa,UAAU,mBAAmB,aAAa,UAAU;EACtH,CAAC,CAAC,CACD,KAAK,YAAY,CAAC,CAClB,MAAM,eAAe;EAExB,OAAO;GACL,kBAAkB,OAAO,cAAc,aAAa,CAAC;GACrD,cAAc,OAAO,cAAc,SAAS,CAAC;GAC7C,gBAAgB,OAAO,mBAAmB,kBAAkB,CAAC;GAC7D,cAAc,OAAO,mBAAmB,gBAAgB,CAAC;EAC3D;CACF;CAIA,MAAM,YAAY,OAAe,MAA0C;EAiBzE,QADa,MAfQ,KAAK,GAAG,QAAQ,GAAG;;;gBAG5B,aAAa,GAAG;;8CAEc,KAAK;;;;kBAIjC,aAAa;aAClB,aAAa,MAAM,KAAK,MAAM;eAC5B,aAAa,UAAU;;;KAGjC,EAAA,CACmB,KACR,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,OAAO,OAAO,EAAE,KAAK;EAAE,EAAE;CACnE;AACF;AACA,kBAAkB,0BAA0B"}
@@ -1,19 +0,0 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Logger } from "@headless-lms/core/shared/ports";
3
- import { ContentRef, LearnEntitlementReader } from "@headless-lms/core/reporting/learn";
4
- //#region src/repositories/learn.d.ts
5
- declare class DrizzleLearnRepository implements LearnEntitlementReader {
6
- private readonly db;
7
- private readonly logger;
8
- constructor(db: NodePgDatabase, logger?: Logger);
9
- private baseFilters;
10
- activeRefs(orgId: string, orgUserId: string): Promise<ContentRef[]>;
11
- activeRef(orgId: string, orgUserId: string, courseId: string): Promise<ContentRef | null>;
12
- private downloadFilters;
13
- activeDownloadRefs(orgId: string, orgUserId: string): Promise<ContentRef[]>;
14
- activeDownloadRef(orgId: string, orgUserId: string, downloadId: string): Promise<ContentRef | null>;
15
- downloadHasAsset(orgId: string, downloadId: string, assetId: string): Promise<boolean>;
16
- }
17
- //#endregion
18
- export { DrizzleLearnRepository };
19
- //# sourceMappingURL=learn.d.ts.map
@@ -1,56 +0,0 @@
1
- import { courses, downloadAssets, downloads } from "../schema/content.js";
2
- import { entitlements } from "../schema/entitlements.js";
3
- import "../schema/index.js";
4
- import { translateDbErrors } from "./pg-errors.js";
5
- import { and, eq, gt, isNull, or, sql } from "drizzle-orm";
6
- import { noopLogger } from "@headless-lms/core/shared/logger";
7
- //#region src/repositories/learn.ts
8
- var DrizzleLearnRepository = class {
9
- db;
10
- logger;
11
- constructor(db, logger = noopLogger) {
12
- this.db = db;
13
- this.logger = logger;
14
- }
15
- baseFilters(orgId, orgUserId) {
16
- return and(eq(entitlements.orgId, orgId), eq(entitlements.orgUserId, orgUserId), eq(entitlements.status, "active"), or(isNull(entitlements.expiresAt), gt(entitlements.expiresAt, sql`now()`)), eq(courses.status, "published"));
17
- }
18
- async activeRefs(orgId, orgUserId) {
19
- return await this.db.select({
20
- orgId: entitlements.orgId,
21
- contentId: courses.id
22
- }).from(entitlements).innerJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).where(this.baseFilters(orgId, orgUserId));
23
- }
24
- async activeRef(orgId, orgUserId, courseId) {
25
- const [row] = await this.db.select({
26
- orgId: entitlements.orgId,
27
- contentId: courses.id
28
- }).from(entitlements).innerJoin(courses, and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId))).where(and(this.baseFilters(orgId, orgUserId), eq(entitlements.contentId, courseId))).limit(1);
29
- return row ?? null;
30
- }
31
- downloadFilters(orgId, orgUserId) {
32
- return and(eq(entitlements.orgId, orgId), eq(entitlements.orgUserId, orgUserId), eq(entitlements.status, "active"), or(isNull(entitlements.expiresAt), gt(entitlements.expiresAt, sql`now()`)), eq(downloads.status, "published"));
33
- }
34
- async activeDownloadRefs(orgId, orgUserId) {
35
- return this.db.select({
36
- orgId: entitlements.orgId,
37
- contentId: downloads.id
38
- }).from(entitlements).innerJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).where(this.downloadFilters(orgId, orgUserId));
39
- }
40
- async activeDownloadRef(orgId, orgUserId, downloadId) {
41
- const [row] = await this.db.select({
42
- orgId: entitlements.orgId,
43
- contentId: downloads.id
44
- }).from(entitlements).innerJoin(downloads, and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId))).where(and(this.downloadFilters(orgId, orgUserId), eq(entitlements.contentId, downloadId))).limit(1);
45
- return row ?? null;
46
- }
47
- async downloadHasAsset(orgId, downloadId, assetId) {
48
- const [row] = await this.db.select({ id: downloadAssets.id }).from(downloadAssets).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId), eq(downloadAssets.assetId, assetId))).limit(1);
49
- return row !== void 0;
50
- }
51
- };
52
- translateDbErrors(DrizzleLearnRepository);
53
- //#endregion
54
- export { DrizzleLearnRepository };
55
-
56
- //# sourceMappingURL=learn.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"learn.js","names":[],"sources":["../../src/repositories/learn.ts"],"sourcesContent":["// learn — Drizzle read repo (implements the reporting/learn outbound port).\n// Returns the (org, content) refs a student is ACTIVELY entitled to and whose\n// course is PUBLISHED. \"Active\" excludes revoked and expired grants (expiry is\n// derived from expires_at at read time — no row flip). Org-scoped: the portal\n// org bounds the read (`entitlements.org_id`), so no cross-org grants leak.\n// The inner join to `courses` on the content id restricts to course grants —\n// no explicit content-type filter needed.\nimport { and, eq, gt, isNull, or, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { LearnEntitlementReader, ContentRef } from \"@headless-lms/core/reporting/learn\";\nimport { entitlements, courses, downloads, downloadAssets } 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\";\n\nexport class DrizzleLearnRepository implements LearnEntitlementReader {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private baseFilters(orgId: string, orgUserId: string): SQL {\n return and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.orgUserId, orgUserId),\n eq(entitlements.status, \"active\"),\n or(isNull(entitlements.expiresAt), gt(entitlements.expiresAt, sql`now()`))!,\n eq(courses.status, \"published\"),\n )!;\n }\n\n async activeRefs(orgId: string, orgUserId: string): Promise<ContentRef[]> {\n const rows = await this.db\n .select({ orgId: entitlements.orgId, contentId: courses.id })\n .from(entitlements)\n .innerJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .where(this.baseFilters(orgId, orgUserId));\n return rows;\n }\n\n async activeRef(orgId: string, orgUserId: string, courseId: string): Promise<ContentRef | null> {\n const [row] = await this.db\n .select({ orgId: entitlements.orgId, contentId: courses.id })\n .from(entitlements)\n .innerJoin(\n courses,\n and(eq(courses.orgId, entitlements.orgId), eq(courses.id, entitlements.contentId)),\n )\n .where(and(this.baseFilters(orgId, orgUserId), eq(entitlements.contentId, courseId)))\n .limit(1);\n return row ?? null;\n }\n\n private downloadFilters(orgId: string, orgUserId: string): SQL {\n return and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.orgUserId, orgUserId),\n eq(entitlements.status, \"active\"),\n or(isNull(entitlements.expiresAt), gt(entitlements.expiresAt, sql`now()`))!,\n eq(downloads.status, \"published\"),\n )!;\n }\n\n async activeDownloadRefs(orgId: string, orgUserId: string): Promise<ContentRef[]> {\n return this.db\n .select({ orgId: entitlements.orgId, contentId: downloads.id })\n .from(entitlements)\n .innerJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .where(this.downloadFilters(orgId, orgUserId));\n }\n\n async activeDownloadRef(\n orgId: string,\n orgUserId: string,\n downloadId: string,\n ): Promise<ContentRef | null> {\n const [row] = await this.db\n .select({ orgId: entitlements.orgId, contentId: downloads.id })\n .from(entitlements)\n .innerJoin(\n downloads,\n and(eq(downloads.orgId, entitlements.orgId), eq(downloads.id, entitlements.contentId)),\n )\n .where(and(this.downloadFilters(orgId, orgUserId), eq(entitlements.contentId, downloadId)))\n .limit(1);\n return row ?? null;\n }\n\n async downloadHasAsset(orgId: string, downloadId: string, assetId: string): Promise<boolean> {\n const [row] = await this.db\n .select({ id: downloadAssets.id })\n .from(downloadAssets)\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n )\n .limit(1);\n return row !== undefined;\n }\n}\ntranslateDbErrors(DrizzleLearnRepository);\n"],"mappings":"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"}
@@ -1,16 +0,0 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Page, Student, StudentAnalytics, StudentsQuery, StudentsReportRepository } from "@headless-lms/core/reporting/students";
4
- //#region src/repositories/students.d.ts
5
- declare class DrizzleStudentsRepository implements StudentsReportRepository {
6
- private readonly db;
7
- private readonly logger;
8
- constructor(db: NodePgDatabase, logger?: Logger);
9
- list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
10
- findById(orgId: string, id: string): Promise<Student | null>;
11
- analytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
12
- private resolveOrder;
13
- }
14
- //#endregion
15
- export { DrizzleStudentsRepository };
16
- //# sourceMappingURL=students.d.ts.map
@@ -1,160 +0,0 @@
1
- import { users } from "../schema/identity.js";
2
- import { orgUsers } from "../schema/organizations.js";
3
- import { entitlements } from "../schema/entitlements.js";
4
- import "../schema/index.js";
5
- import { translateDbErrors } from "./pg-errors.js";
6
- import { user } from "../schema/better-auth.js";
7
- import { orgUserProfileColumns } from "./org-user-profile.js";
8
- import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
9
- import { noopLogger } from "@headless-lms/core/shared/logger";
10
- import { STUDENT_ROLE } from "@headless-lms/core/organizations";
11
- //#region src/repositories/students.ts
12
- const entitlementCountExpr = sql`count(${entitlements.id})`;
13
- const avgProgressExpr = sql`0`;
14
- const lastActiveExpr = sql`(
15
- select max(pr.updated_at) from progress_records pr
16
- where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}
17
- )`;
18
- function toStudent(row) {
19
- return {
20
- id: row.id,
21
- email: row.email,
22
- image: row.image ?? null,
23
- firstName: row.firstName,
24
- lastName: row.lastName,
25
- status: row.status,
26
- entitlementCount: Number(row.entitlementCount),
27
- avgProgress: Number(row.avgProgress),
28
- joinedAt: row.createdAt.toISOString(),
29
- lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null
30
- };
31
- }
32
- var DrizzleStudentsRepository = class {
33
- db;
34
- logger;
35
- constructor(db, logger = noopLogger) {
36
- this.db = db;
37
- this.logger = logger;
38
- }
39
- async list(orgId, query) {
40
- const filters = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];
41
- const q = query.search?.trim();
42
- if (q) {
43
- const like = `%${q}%`;
44
- filters.push(or(ilike(users.firstName, like), ilike(users.lastName, like), ilike(users.email, like)));
45
- }
46
- const where = and(...filters);
47
- const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
48
- return {
49
- rows: (await this.db.select({
50
- ...orgUserProfileColumns,
51
- status: orgUsers.status,
52
- createdAt: orgUsers.createdAt,
53
- entitlementCount: entitlementCountExpr,
54
- avgProgress: avgProgressExpr,
55
- lastActiveAt: lastActiveExpr
56
- }).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.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map(toStudent),
57
- total: Number(totals?.total ?? 0),
58
- page: query.page,
59
- pageSize: query.pageSize
60
- };
61
- }
62
- async findById(orgId, id) {
63
- const [row] = await this.db.select({
64
- ...orgUserProfileColumns,
65
- status: orgUsers.status,
66
- createdAt: orgUsers.createdAt,
67
- entitlementCount: entitlementCountExpr,
68
- avgProgress: avgProgressExpr,
69
- lastActiveAt: lastActiveExpr
70
- }).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), eq(orgUsers.role, STUDENT_ROLE))).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.email, users.firstName, users.lastName, user.image).limit(1);
71
- return row ? toStudent(row) : null;
72
- }
73
- async analytics(orgId, id) {
74
- const [exists] = await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)));
75
- if (!exists) return null;
76
- const courses = (await this.db.execute(sql`
77
- with cohort_courses as (
78
- select c.id, c.title
79
- from entitlements e
80
- join courses c on c.org_id = e.org_id and c.id = e.content_id
81
- where e.org_id = ${orgId}
82
- and e.org_user_id = ${id}
83
- and e.status = 'active'
84
- and (e.expires_at is null or e.expires_at >= now())
85
- ),
86
- acts as (
87
- select a.course_id, a.id
88
- from activities a
89
- where a.org_id = ${orgId}
90
- and a.course_id in (select id from cohort_courses)
91
- and coalesce((a.settings ->> 'published')::boolean, true)
92
- )
93
- select
94
- cc.id as course_id,
95
- cc.title,
96
- count(a.id)::int as total_activities,
97
- count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,
98
- min(pr.started_at) as started_at,
99
- max(pr.updated_at) as last_activity_at,
100
- (
101
- select prc.completed_at from progress_records prc
102
- where prc.org_id = ${orgId}
103
- and prc.org_user_id = ${id}
104
- and prc.target_type = 'course'
105
- and prc.target_id = cc.id
106
- ) as completed_at
107
- from cohort_courses cc
108
- left join acts a on a.course_id = cc.id
109
- left join progress_records pr
110
- on pr.org_id = ${orgId}
111
- and pr.org_user_id = ${id}
112
- and pr.target_type = 'activity'
113
- and pr.target_id = a.id
114
- group by cc.id, cc.title
115
- order by cc.title
116
- `)).rows.map((r) => {
117
- const total = Number(r.total_activities);
118
- const done = Number(r.completed_activities);
119
- return {
120
- courseId: r.course_id,
121
- title: r.title,
122
- totalActivities: total,
123
- completedActivities: done,
124
- progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,
125
- startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,
126
- lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,
127
- completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null
128
- };
129
- });
130
- const started = courses.filter((c) => c.startedAt !== null).length;
131
- const completed = courses.filter((c) => c.completedAt !== null).length;
132
- const avgProgress = courses.length > 0 ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length) : 0;
133
- return {
134
- enrolled: courses.length,
135
- started,
136
- completed,
137
- avgProgress,
138
- courses
139
- };
140
- }
141
- resolveOrder(sort) {
142
- const descending = sort?.startsWith("-") ?? false;
143
- const field = sort ? descending ? sort.slice(1) : sort : "firstName";
144
- const dir = descending ? desc : asc;
145
- switch (field) {
146
- case "email": return [dir(users.email)];
147
- case "entitlementCount": return [dir(entitlementCountExpr)];
148
- case "avgProgress": return [dir(avgProgressExpr)];
149
- case "joinedAt": return [dir(orgUsers.createdAt)];
150
- case "lastActiveAt": return [dir(lastActiveExpr)];
151
- case "lastName": return [dir(users.lastName), dir(users.firstName)];
152
- default: return [dir(users.firstName), dir(users.lastName)];
153
- }
154
- }
155
- };
156
- translateDbErrors(DrizzleStudentsRepository);
157
- //#endregion
158
- export { DrizzleStudentsRepository };
159
-
160
- //# sourceMappingURL=students.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"students.js","names":[],"sources":["../../src/repositories/students.ts"],"sourcesContent":["// students — Drizzle repository (implements the core outbound port).\n// A read-model over org users + entitlements: a \"student\" row in the report is\n// any org_users row with role 'student', with or without entitlements. Rooted at\n// `org_users` and scoped by its org, with entitlements LEFT JOINed in for the\n// aggregated count. Name/email come from the identity `users` row (INNER, since\n// user_id is NOT NULL); the avatar comes from the better-auth `user` table,\n// LEFT JOINed via that same identity row.\nimport { and, asc, desc, eq, ilike, or, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n Page,\n Student,\n StudentAnalytics,\n StudentCourseProgress,\n StudentsQuery,\n StudentsReportRepository,\n} from \"@headless-lms/core/reporting/students\";\nimport { orgUsers, users, entitlements } 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 { orgUserProfileColumns } from \"./org-user-profile.js\";\nimport { translateDbErrors } from \"./pg-errors.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 DrizzleStudentsRepository implements StudentsReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async list(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(users.firstName, like), ilike(users.lastName, 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 ...orgUserProfileColumns,\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.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 findById(orgId: string, id: string): Promise<Student | null> {\n const [row] = await this.db\n .select({\n ...orgUserProfileColumns,\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.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 reporting/courses does.\n async analytics(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 c.id, c.title\n from entitlements e\n join courses c on c.org_id = e.org_id and c.id = e.content_id\n where e.org_id = ${orgId}\n and e.org_user_id = ${id}\n and e.status = 'active'\n and (e.expires_at is null or e.expires_at >= now())\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(users.lastName), dir(users.firstName)];\n case \"firstName\":\n default:\n return [dir(users.firstName), dir(users.lastName)];\n }\n }\n}\ntranslateDbErrors(DrizzleStudentsRepository);\n"],"mappings":"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"}