@headless-lms/adapter-db 0.1.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 (138) hide show
  1. package/README.md +79 -0
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +7 -5
  5. package/dist/index.js +8 -6
  6. package/dist/migrate.js.map +1 -1
  7. package/dist/repositories/admin.d.ts +23 -0
  8. package/dist/repositories/admin.js +393 -0
  9. package/dist/repositories/admin.js.map +1 -0
  10. package/dist/repositories/assets.d.ts +6 -2
  11. package/dist/repositories/assets.js +53 -9
  12. package/dist/repositories/assets.js.map +1 -1
  13. package/dist/repositories/automations.d.ts +12 -6
  14. package/dist/repositories/automations.js +79 -11
  15. package/dist/repositories/automations.js.map +1 -1
  16. package/dist/repositories/chats.d.ts +55 -0
  17. package/dist/repositories/chats.js +454 -0
  18. package/dist/repositories/chats.js.map +1 -0
  19. package/dist/repositories/content.d.ts +44 -7
  20. package/dist/repositories/content.js +458 -24
  21. package/dist/repositories/content.js.map +1 -1
  22. package/dist/repositories/credentials.js +2 -0
  23. package/dist/repositories/credentials.js.map +1 -1
  24. package/dist/repositories/discussion.d.ts +11 -4
  25. package/dist/repositories/discussion.js +72 -15
  26. package/dist/repositories/discussion.js.map +1 -1
  27. package/dist/repositories/entitlements.d.ts +36 -10
  28. package/dist/repositories/entitlements.js +238 -23
  29. package/dist/repositories/entitlements.js.map +1 -1
  30. package/dist/repositories/grant-covers.d.ts +20 -0
  31. package/dist/repositories/grant-covers.js +55 -0
  32. package/dist/repositories/grant-covers.js.map +1 -0
  33. package/dist/repositories/identity.d.ts +4 -2
  34. package/dist/repositories/identity.js +34 -1
  35. package/dist/repositories/identity.js.map +1 -1
  36. package/dist/repositories/integrations.d.ts +1 -1
  37. package/dist/repositories/integrations.js +2 -0
  38. package/dist/repositories/integrations.js.map +1 -1
  39. package/dist/repositories/member.d.ts +16 -0
  40. package/dist/repositories/member.js +63 -0
  41. package/dist/repositories/member.js.map +1 -0
  42. package/dist/repositories/members.js +4 -2
  43. package/dist/repositories/members.js.map +1 -1
  44. package/dist/repositories/notifications.d.ts +23 -0
  45. package/dist/repositories/notifications.js +70 -0
  46. package/dist/repositories/notifications.js.map +1 -0
  47. package/dist/repositories/org-user-profile.d.ts +214 -1
  48. package/dist/repositories/org-user-profile.js +23 -2
  49. package/dist/repositories/org-user-profile.js.map +1 -1
  50. package/dist/repositories/organizations.d.ts +6 -7
  51. package/dist/repositories/organizations.js +45 -52
  52. package/dist/repositories/organizations.js.map +1 -1
  53. package/dist/repositories/outbox.js +3 -0
  54. package/dist/repositories/outbox.js.map +1 -1
  55. package/dist/repositories/pg-errors.d.ts +53 -5
  56. package/dist/repositories/pg-errors.js +99 -8
  57. package/dist/repositories/pg-errors.js.map +1 -1
  58. package/dist/repositories/posts.d.ts +39 -0
  59. package/dist/repositories/posts.js +255 -0
  60. package/dist/repositories/posts.js.map +1 -0
  61. package/dist/repositories/progress.d.ts +1 -1
  62. package/dist/repositories/progress.js +3 -0
  63. package/dist/repositories/progress.js.map +1 -1
  64. package/dist/repositories/settings.js +2 -0
  65. package/dist/repositories/settings.js.map +1 -1
  66. package/dist/schema/assets.d.ts +36 -2
  67. package/dist/schema/assets.js +12 -5
  68. package/dist/schema/assets.js.map +1 -1
  69. package/dist/schema/automations.d.ts +123 -8
  70. package/dist/schema/automations.js +11 -4
  71. package/dist/schema/automations.js.map +1 -1
  72. package/dist/schema/better-auth.d.ts +1613 -238
  73. package/dist/schema/better-auth.js +107 -22
  74. package/dist/schema/better-auth.js.map +1 -1
  75. package/dist/schema/content.d.ts +909 -22
  76. package/dist/schema/content.js +157 -13
  77. package/dist/schema/content.js.map +1 -1
  78. package/dist/schema/credentials.js +1 -1
  79. package/dist/schema/credentials.js.map +1 -1
  80. package/dist/schema/discussion.d.ts +2079 -55
  81. package/dist/schema/discussion.js +430 -13
  82. package/dist/schema/discussion.js.map +1 -1
  83. package/dist/schema/entitlements.d.ts +484 -2
  84. package/dist/schema/entitlements.js +121 -6
  85. package/dist/schema/entitlements.js.map +1 -1
  86. package/dist/schema/identity.d.ts +517 -1
  87. package/dist/schema/identity.js +118 -2
  88. package/dist/schema/identity.js.map +1 -1
  89. package/dist/schema/index.d.ts +6 -6
  90. package/dist/schema/index.js +32 -7
  91. package/dist/schema/index.js.map +1 -1
  92. package/dist/schema/integrations.js +1 -1
  93. package/dist/schema/integrations.js.map +1 -1
  94. package/dist/schema/organizations.d.ts +21 -4
  95. package/dist/schema/organizations.js +4 -1
  96. package/dist/schema/organizations.js.map +1 -1
  97. package/dist/schema/outbox.d.ts +17 -0
  98. package/dist/schema/outbox.js +1 -0
  99. package/dist/schema/outbox.js.map +1 -1
  100. package/dist/schema/progress.d.ts +17 -0
  101. package/dist/schema/progress.js +1 -0
  102. package/dist/schema/progress.js.map +1 -1
  103. package/dist/schema/settings.js.map +1 -1
  104. package/dist/unit-of-work.js +7 -2
  105. package/dist/unit-of-work.js.map +1 -1
  106. package/drizzle/0000_baseline.sql +0 -1
  107. package/drizzle/0001_new_sentry.sql +43 -0
  108. package/drizzle/0002_automation_kinds.sql +7 -0
  109. package/drizzle/0003_community.sql +340 -0
  110. package/drizzle/0004_assets_sources.sql +1 -0
  111. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  112. package/drizzle/0006_community_review_fixes.sql +24 -0
  113. package/drizzle/0007_notifications.sql +37 -0
  114. package/drizzle/0008_content_access.sql +13 -0
  115. package/drizzle/0009_community_indexes.sql +56 -0
  116. package/drizzle/0010_mcp_access.sql +114 -0
  117. package/drizzle/meta/0000_snapshot.json +167 -597
  118. package/drizzle/meta/0001_snapshot.json +3769 -0
  119. package/drizzle/meta/0002_snapshot.json +3770 -0
  120. package/drizzle/meta/0003_snapshot.json +7061 -0
  121. package/drizzle/meta/0004_snapshot.json +7068 -0
  122. package/drizzle/meta/0005_snapshot.json +7180 -0
  123. package/drizzle/meta/0006_snapshot.json +7298 -0
  124. package/drizzle/meta/0007_snapshot.json +7620 -0
  125. package/drizzle/meta/0008_snapshot.json +7666 -0
  126. package/drizzle/meta/0009_snapshot.json +8185 -0
  127. package/drizzle/meta/0010_snapshot.json +8813 -0
  128. package/drizzle/meta/_journal.json +71 -1
  129. package/package.json +3 -2
  130. package/dist/repositories/dashboard.d.ts +0 -14
  131. package/dist/repositories/dashboard.js +0 -55
  132. package/dist/repositories/dashboard.js.map +0 -1
  133. package/dist/repositories/learn.d.ts +0 -19
  134. package/dist/repositories/learn.js +0 -54
  135. package/dist/repositories/learn.js.map +0 -1
  136. package/dist/repositories/students.d.ts +0 -15
  137. package/dist/repositories/students.js +0 -83
  138. package/dist/repositories/students.js.map +0 -1
@@ -0,0 +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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"}
@@ -9,8 +9,12 @@ declare class DrizzleAssetsRepository implements AssetsRepository {
9
9
  insert(orgId: string, asset: Asset): Promise<Asset>;
10
10
  list(orgId: string, query: AssetsQuery): Promise<Page<Asset>>;
11
11
  findById(orgId: string, id: string): Promise<Asset | null>;
12
- update(id: string, patch: Partial<Pick<Asset, 'size' | 'contentType' | 'status'>>): Promise<Asset | null>;
13
- delete(id: string): Promise<boolean>;
12
+ findByIds(orgId: string, ids: string[]): Promise<Asset[]>;
13
+ markReady(orgId: string, id: string, patch: Pick<Asset, "key" | "kind" | "size" | "contentType">): Promise<Asset | null>;
14
+ delete(orgId: string, id: string): Promise<boolean>;
15
+ hasUnreferencedMemberUploads(orgId: string, before: Date): Promise<boolean>;
16
+ listUnreferencedMemberUploads(orgId: string, before: Date): Promise<Asset[]>;
17
+ heldKeys(orgId: string, keys: string[]): Promise<string[]>;
14
18
  }
15
19
  //#endregion
16
20
  export { DrizzleAssetsRepository };
@@ -1,5 +1,7 @@
1
1
  import { assets } from "../schema/assets.js";
2
- import { and, asc, desc, eq, ilike, sql } from "drizzle-orm";
2
+ import { pgErrorFields, translateDbErrors } from "./pg-errors.js";
3
+ import { and, asc, desc, eq, ilike, inArray, lt, sql } from "drizzle-orm";
4
+ import { ConflictError } from "@headless-lms/core/shared/errors";
3
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
6
  //#region src/repositories/assets.ts
5
7
  function toAsset(row) {
@@ -8,12 +10,14 @@ function toAsset(row) {
8
10
  id: row.id,
9
11
  key: row.key,
10
12
  kind: row.kind,
13
+ source: row.source,
11
14
  filename: row.filename,
12
15
  contentType: row.contentType,
13
16
  size: row.size,
14
17
  status: row.status,
15
18
  uploadedBy: row.uploadedBy,
16
- createdAt: row.createdAt
19
+ createdAt: row.createdAt,
20
+ updatedAt: row.updatedAt
17
21
  };
18
22
  }
19
23
  const SORT_COLUMNS = {
@@ -21,6 +25,21 @@ const SORT_COLUMNS = {
21
25
  size: assets.size,
22
26
  createdAt: assets.createdAt
23
27
  };
28
+ function referenced(orgId, assetId) {
29
+ return sql`(
30
+ exists (select 1 from user_profiles p where p.image_asset_org_id = ${orgId} and p.image_asset_id = ${assetId})
31
+ or exists (select 1 from posts p where p.org_id = ${orgId} and p.cover_asset_id = ${assetId})
32
+ or exists (select 1 from message_attachments ma where ma.org_id = ${orgId} and ma.asset_id = ${assetId})
33
+ or exists (select 1 from activity_assets aa where aa.org_id = ${orgId} and aa.asset_id = ${assetId})
34
+ or exists (select 1 from download_assets da where da.org_id = ${orgId} and da.asset_id = ${assetId})
35
+ or exists (select 1 from courses c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})
36
+ or exists (select 1 from downloads d where d.org_id = ${orgId} and d.thumbnail_asset_id = ${assetId})
37
+ or exists (select 1 from communities c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})
38
+ )`;
39
+ }
40
+ function unreferencedMemberUploads(orgId, before) {
41
+ return and(eq(assets.orgId, orgId), eq(assets.source, "member"), lt(assets.createdAt, before), sql`not ${referenced(sql`${assets.orgId}`, sql`${assets.id}`)}`);
42
+ }
24
43
  var DrizzleAssetsRepository = class {
25
44
  db;
26
45
  logger;
@@ -35,6 +54,7 @@ var DrizzleAssetsRepository = class {
35
54
  orgId: asset.orgId,
36
55
  key: asset.key,
37
56
  kind: asset.kind,
57
+ source: asset.source,
38
58
  filename: asset.filename,
39
59
  contentType: asset.contentType,
40
60
  size: asset.size,
@@ -48,6 +68,7 @@ var DrizzleAssetsRepository = class {
48
68
  async list(orgId, query) {
49
69
  const filters = [eq(assets.orgId, orgId)];
50
70
  if (query.kind) filters.push(eq(assets.kind, query.kind));
71
+ if (query.source) filters.push(eq(assets.source, query.source));
51
72
  const q = query.search?.trim();
52
73
  if (q) filters.push(ilike(assets.filename, `%${q}%`));
53
74
  const where = and(...filters);
@@ -72,18 +93,41 @@ var DrizzleAssetsRepository = class {
72
93
  const [row] = await this.db.select().from(assets).where(and(eq(assets.orgId, orgId), eq(assets.id, id))).limit(1);
73
94
  return row ? toAsset(row) : null;
74
95
  }
75
- async update(id, patch) {
96
+ async findByIds(orgId, ids) {
97
+ if (ids.length === 0) return [];
98
+ return (await this.db.select().from(assets).where(and(eq(assets.orgId, orgId), inArray(assets.id, ids)))).map(toAsset);
99
+ }
100
+ async markReady(orgId, id, patch) {
76
101
  const [row] = await this.db.update(assets).set({
77
- ...patch.size !== void 0 ? { size: patch.size } : {},
78
- ...patch.contentType !== void 0 ? { contentType: patch.contentType } : {},
79
- ...patch.status !== void 0 ? { status: patch.status } : {}
80
- }).where(eq(assets.id, id)).returning();
102
+ key: patch.key,
103
+ kind: patch.kind,
104
+ size: patch.size,
105
+ contentType: patch.contentType,
106
+ status: "ready"
107
+ }).where(and(eq(assets.orgId, orgId), eq(assets.id, id), eq(assets.status, "pending"))).returning();
81
108
  return row ? toAsset(row) : null;
82
109
  }
83
- async delete(id) {
84
- return (await this.db.delete(assets).where(eq(assets.id, id)).returning({ id: assets.id })).length > 0;
110
+ async delete(orgId, id) {
111
+ try {
112
+ return (await this.db.delete(assets).where(and(eq(assets.orgId, orgId), eq(assets.id, id))).returning({ id: assets.id })).length > 0;
113
+ } catch (err) {
114
+ if (pgErrorFields(err)?.code === "23503") throw Object.assign(new ConflictError("This file is still in use"), { cause: err });
115
+ throw err;
116
+ }
117
+ }
118
+ async hasUnreferencedMemberUploads(orgId, before) {
119
+ const [row] = await this.db.select({ id: assets.id }).from(assets).where(unreferencedMemberUploads(orgId, before)).limit(1);
120
+ return row !== void 0;
121
+ }
122
+ async listUnreferencedMemberUploads(orgId, before) {
123
+ return (await this.db.select().from(assets).where(unreferencedMemberUploads(orgId, before)).orderBy(asc(assets.createdAt))).map(toAsset);
124
+ }
125
+ async heldKeys(orgId, keys) {
126
+ if (keys.length === 0) return [];
127
+ return (await this.db.select({ key: assets.key }).from(assets).where(and(eq(assets.orgId, orgId), inArray(assets.key, keys)))).map((r) => r.key);
85
128
  }
86
129
  };
130
+ translateDbErrors(DrizzleAssetsRepository);
87
131
  //#endregion
88
132
  export { DrizzleAssetsRepository };
89
133
 
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["// assets — Drizzle repository (implements the core outbound port).\nimport { and, eq, ilike, sql, asc, desc, type SQL } from 'drizzle-orm';\nimport type { AssetsRepository } from '@headless-lms/core/assets';\nimport type {\n Asset,\n AssetKind,\n AssetStatus,\n AssetsQuery,\n Logger,\n Page,\n} from '@headless-lms/core/types';\nimport { assets } from '../schema/assets.js';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport type { DbExecutor } from '../client.js';\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n createdAt: row.createdAt,\n };\n}\n\n// Whitelisted sort columns; anything else falls back to newest-first.\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error('asset org mismatch');\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error('failed to insert asset');\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith('-');\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async update(\n id: string,\n patch: Partial<Pick<Asset, 'size' | 'contentType' | 'status'>>,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n ...(patch.size !== undefined ? { size: patch.size } : {}),\n ...(patch.contentType !== undefined ? { contentType: patch.contentType } : {}),\n ...(patch.status !== undefined ? { status: patch.status } : {}),\n })\n .where(eq(assets.id, id))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(id: string): Promise<boolean> {\n const deleted = await this.db\n .delete(assets)\n .where(eq(assets.id, id))\n .returning({ id: assets.id });\n return deleted.length > 0;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["import { and, eq, ilike, inArray, lt, sql, asc, desc, type SQL } from \"drizzle-orm\";\nimport type { AssetsRepository } from \"@headless-lms/core/assets\";\nimport type {\n Asset,\n AssetKind,\n AssetSource,\n AssetStatus,\n AssetsQuery,\n Logger,\n Page,\n} from \"@headless-lms/core/types\";\nimport { assets } from \"../schema/assets.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { DbExecutor } from \"../client.js\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n source: row.source as AssetSource,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nfunction referenced(orgId: SQL, assetId: SQL): SQL {\n return sql`(\n exists (select 1 from user_profiles p where p.image_asset_org_id = ${orgId} and p.image_asset_id = ${assetId})\n or exists (select 1 from posts p where p.org_id = ${orgId} and p.cover_asset_id = ${assetId})\n or exists (select 1 from message_attachments ma where ma.org_id = ${orgId} and ma.asset_id = ${assetId})\n or exists (select 1 from activity_assets aa where aa.org_id = ${orgId} and aa.asset_id = ${assetId})\n or exists (select 1 from download_assets da where da.org_id = ${orgId} and da.asset_id = ${assetId})\n or exists (select 1 from courses c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n or exists (select 1 from downloads d where d.org_id = ${orgId} and d.thumbnail_asset_id = ${assetId})\n or exists (select 1 from communities c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n )`;\n}\n\nfunction unreferencedMemberUploads(orgId: string, before: Date): SQL | undefined {\n return and(\n eq(assets.orgId, orgId),\n eq(assets.source, \"member\"),\n lt(assets.createdAt, before),\n sql`not ${referenced(sql`${assets.orgId}`, sql`${assets.id}`)}`,\n );\n}\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error(\"asset org mismatch\");\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n source: asset.source,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert asset\");\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n if (query.source) {\n filters.push(eq(assets.source, query.source));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith(\"-\");\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async findByIds(orgId: string, ids: string[]): Promise<Asset[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.id, ids)));\n return rows.map(toAsset);\n }\n\n async markReady(\n orgId: string,\n id: string,\n patch: Pick<Asset, \"key\" | \"kind\" | \"size\" | \"contentType\">,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n key: patch.key,\n kind: patch.kind,\n size: patch.size,\n contentType: patch.contentType,\n status: \"ready\",\n })\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id), eq(assets.status, \"pending\")))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n try {\n const deleted = await this.db\n .delete(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .returning({ id: assets.id });\n return deleted.length > 0;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(new ConflictError(\"This file is still in use\"), { cause: err });\n }\n throw err;\n }\n }\n\n async hasUnreferencedMemberUploads(orgId: string, before: Date): Promise<boolean> {\n const [row] = await this.db\n .select({ id: assets.id })\n .from(assets)\n .where(unreferencedMemberUploads(orgId, before))\n .limit(1);\n return row !== undefined;\n }\n\n async listUnreferencedMemberUploads(orgId: string, before: Date): Promise<Asset[]> {\n const rows = await this.db\n .select()\n .from(assets)\n .where(unreferencedMemberUploads(orgId, before))\n .orderBy(asc(assets.createdAt));\n return rows.map(toAsset);\n }\n\n async heldKeys(orgId: string, keys: string[]): Promise<string[]> {\n if (keys.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ key: assets.key })\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.key, keys)));\n return rows.map((r) => r.key);\n }\n}\ntranslateDbErrors(DrizzleAssetsRepository);\n"],"mappings":"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"}
@@ -1,29 +1,35 @@
1
1
  import { DbExecutor } from "../client.js";
2
2
  import { Logger } from "@headless-lms/core/shared/ports";
3
- import { Automation, AutomationRun, AutomationRunsQuery, AutomationRunsRepository, AutomationsRepository, CreateAutomationInput, NewAutomationRun, Page, UpdateAutomationInput } from "@headless-lms/core/automations";
3
+ import { ScheduledAutomation } from "@headless-lms/core/types";
4
+ import { Automation, AutomationKind, AutomationRun, AutomationRunsQuery, AutomationRunsRepository, AutomationsRepository, NewAutomation, NewAutomationRun, Page as Page$1, UpdateAutomationInput } from "@headless-lms/core/automations";
4
5
  //#region src/repositories/automations.d.ts
5
6
  declare class DrizzleAutomationsRepository implements AutomationsRepository {
6
7
  private readonly db;
7
8
  private readonly logger;
8
9
  constructor(db: DbExecutor, logger?: Logger);
9
- insert(orgId: string, input: CreateAutomationInput): Promise<Automation>;
10
+ insert(orgId: string, input: NewAutomation): Promise<Automation>;
11
+ insertSeed(orgId: string, input: NewAutomation): Promise<Automation | null>;
10
12
  update(orgId: string, id: string, patch: UpdateAutomationInput): Promise<Automation | null>;
11
13
  delete(orgId: string, id: string): Promise<Automation | null>;
12
14
  findById(orgId: string, id: string): Promise<Automation | null>;
13
- listByOrg(orgId: string): Promise<Automation[]>;
15
+ listByOrg(orgId: string, kind?: AutomationKind): Promise<Automation[]>;
14
16
  listByTrigger(orgId: string, trigger: string): Promise<Automation[]>;
17
+ listSchedules(): Promise<ScheduledAutomation[]>;
15
18
  }
16
19
  declare class DrizzleAutomationRunsRepository implements AutomationRunsRepository {
17
20
  private readonly db;
18
21
  private readonly logger;
19
22
  constructor(db: DbExecutor, logger?: Logger);
20
23
  insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null>;
24
+ findById(orgId: string, id: string): Promise<AutomationRun | null>;
25
+ restart(orgId: string, id: string, startedAt: Date): Promise<AutomationRun | null>;
21
26
  recordOutcome(orgId: string, id: string, outcome: {
22
- status: AutomationRun['status'];
23
- actionResults: AutomationRun['actionResults'];
27
+ status: AutomationRun["status"];
28
+ actionResults: AutomationRun["actionResults"];
24
29
  finishedAt: Date;
25
30
  }): Promise<AutomationRun | null>;
26
- list(orgId: string, automationId: string, query: AutomationRunsQuery): Promise<Page<AutomationRun>>;
31
+ list(orgId: string, automationId: string, query: AutomationRunsQuery): Promise<Page$1<AutomationRun>>;
32
+ recentRecipients(orgId: string, automationId: string, chatId: string, since: Date): Promise<string[]>;
27
33
  }
28
34
  //#endregion
29
35
  export { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository };
@@ -1,16 +1,20 @@
1
1
  import { automationRuns, automations } from "../schema/automations.js";
2
2
  import "../schema/index.js";
3
- import { and, asc, desc, eq, sql } from "drizzle-orm";
3
+ import { translateDbErrors } from "./pg-errors.js";
4
+ import { and, arrayContains, asc, desc, eq, gte, ne, sql } from "drizzle-orm";
4
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
5
6
  //#region src/repositories/automations.ts
6
7
  const automationSelection = {
7
8
  orgId: automations.orgId,
8
9
  id: automations.id,
9
10
  name: automations.name,
11
+ kind: automations.kind,
10
12
  description: automations.description,
11
- trigger: automations.trigger,
13
+ triggers: automations.triggers,
12
14
  actions: automations.actions,
13
15
  enabled: automations.enabled,
16
+ filter: automations.filter,
17
+ source: automations.source,
14
18
  createdAt: automations.createdAt,
15
19
  updatedAt: automations.updatedAt
16
20
  };
@@ -19,10 +23,13 @@ function toAutomation(row) {
19
23
  orgId: row.orgId,
20
24
  id: row.id,
21
25
  name: row.name,
26
+ kind: row.kind,
22
27
  description: row.description,
23
- trigger: row.trigger,
28
+ triggers: row.triggers,
24
29
  actions: row.actions,
25
30
  enabled: row.enabled,
31
+ filter: row.filter,
32
+ source: row.source,
26
33
  createdAt: row.createdAt,
27
34
  updatedAt: row.updatedAt
28
35
  };
@@ -38,22 +45,39 @@ var DrizzleAutomationsRepository = class {
38
45
  const [inserted] = await this.db.insert(automations).values({
39
46
  orgId,
40
47
  name: input.name,
48
+ kind: input.kind,
41
49
  description: input.description,
42
- trigger: input.trigger,
43
- actions: input.actions
50
+ triggers: input.triggers,
51
+ actions: input.actions,
52
+ filter: input.filter ?? null,
53
+ source: input.source ?? "user"
44
54
  }).returning({ id: automations.id });
45
55
  if (!inserted) throw new Error("failed to insert automation");
46
56
  const created = await this.findById(orgId, inserted.id);
47
57
  if (!created) throw new Error("failed to load created automation");
48
58
  return created;
49
59
  }
60
+ async insertSeed(orgId, input) {
61
+ const [inserted] = await this.db.insert(automations).values({
62
+ orgId,
63
+ name: input.name,
64
+ kind: input.kind,
65
+ description: input.description,
66
+ triggers: input.triggers,
67
+ actions: input.actions,
68
+ filter: input.filter ?? null,
69
+ source: "system"
70
+ }).onConflictDoNothing().returning({ id: automations.id });
71
+ return inserted ? this.findById(orgId, inserted.id) : null;
72
+ }
50
73
  async update(orgId, id, patch) {
51
74
  const set = { updatedAt: /* @__PURE__ */ new Date() };
52
75
  if (patch.name !== void 0) set.name = patch.name;
53
76
  if (patch.description !== void 0) set.description = patch.description;
54
- if (patch.trigger !== void 0) set.trigger = patch.trigger;
77
+ if (patch.triggers !== void 0) set.triggers = patch.triggers;
55
78
  if (patch.actions !== void 0) set.actions = patch.actions;
56
79
  if (patch.enabled !== void 0) set.enabled = patch.enabled;
80
+ if (patch.filter !== void 0) set.filter = patch.filter;
57
81
  const [updated] = await this.db.update(automations).set(set).where(and(eq(automations.orgId, orgId), eq(automations.id, id))).returning({ id: automations.id });
58
82
  if (!updated) return null;
59
83
  return this.findById(orgId, id);
@@ -66,11 +90,30 @@ var DrizzleAutomationsRepository = class {
66
90
  const [row] = await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), eq(automations.id, id))).limit(1);
67
91
  return row ? toAutomation(row) : null;
68
92
  }
69
- async listByOrg(orgId) {
70
- return (await this.db.select(automationSelection).from(automations).where(eq(automations.orgId, orgId)).orderBy(desc(automations.createdAt))).map(toAutomation);
93
+ async listByOrg(orgId, kind) {
94
+ const conditions = [eq(automations.orgId, orgId)];
95
+ if (kind) conditions.push(eq(automations.kind, kind));
96
+ return (await this.db.select(automationSelection).from(automations).where(and(...conditions)).orderBy(desc(automations.createdAt))).map(toAutomation);
71
97
  }
72
98
  async listByTrigger(orgId, trigger) {
73
- return (await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), eq(automations.trigger, trigger))).orderBy(desc(automations.createdAt))).map(toAutomation);
99
+ return (await this.db.select(automationSelection).from(automations).where(and(eq(automations.orgId, orgId), arrayContains(automations.triggers, [trigger]))).orderBy(desc(automations.createdAt))).map(toAutomation);
100
+ }
101
+ async listSchedules() {
102
+ const rows = await this.db.select({
103
+ orgId: automations.orgId,
104
+ id: automations.id,
105
+ triggers: automations.triggers
106
+ }).from(automations).where(and(eq(automations.enabled, true), sql`exists (select 1 from unnest(${automations.triggers}) as t(trigger) where t.trigger like 'schedule:%')`));
107
+ const schedules = [];
108
+ for (const row of rows) for (const trigger of row.triggers) {
109
+ if (!trigger.startsWith("schedule:")) continue;
110
+ schedules.push({
111
+ orgId: row.orgId,
112
+ automationId: row.id,
113
+ cron: trigger.slice(9)
114
+ });
115
+ }
116
+ return schedules;
74
117
  }
75
118
  };
76
119
  const runSelection = {
@@ -83,7 +126,9 @@ const runSelection = {
83
126
  status: automationRuns.status,
84
127
  actionResults: automationRuns.actionResults,
85
128
  startedAt: automationRuns.startedAt,
86
- finishedAt: automationRuns.finishedAt
129
+ finishedAt: automationRuns.finishedAt,
130
+ createdAt: automationRuns.createdAt,
131
+ updatedAt: automationRuns.updatedAt
87
132
  };
88
133
  function toAutomationRun(row) {
89
134
  return {
@@ -96,7 +141,9 @@ function toAutomationRun(row) {
96
141
  status: row.status,
97
142
  actionResults: row.actionResults,
98
143
  startedAt: row.startedAt,
99
- finishedAt: row.finishedAt
144
+ finishedAt: row.finishedAt,
145
+ createdAt: row.createdAt,
146
+ updatedAt: row.updatedAt
100
147
  };
101
148
  }
102
149
  const sortColumns = {
@@ -129,6 +176,19 @@ var DrizzleAutomationRunsRepository = class {
129
176
  ] }).returning(runSelection);
130
177
  return inserted ? toAutomationRun(inserted) : null;
131
178
  }
179
+ async findById(orgId, id) {
180
+ const [row] = await this.db.select(runSelection).from(automationRuns).where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id))).limit(1);
181
+ return row ? toAutomationRun(row) : null;
182
+ }
183
+ async restart(orgId, id, startedAt) {
184
+ const [restarted] = await this.db.update(automationRuns).set({
185
+ status: "running",
186
+ actionResults: [],
187
+ startedAt,
188
+ finishedAt: null
189
+ }).where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id), ne(automationRuns.status, "running"))).returning(runSelection);
190
+ return restarted ? toAutomationRun(restarted) : null;
191
+ }
132
192
  async recordOutcome(orgId, id, outcome) {
133
193
  const [updated] = await this.db.update(automationRuns).set({
134
194
  status: outcome.status,
@@ -158,7 +218,15 @@ var DrizzleAutomationRunsRepository = class {
158
218
  pageSize: query.pageSize
159
219
  };
160
220
  }
221
+ async recentRecipients(orgId, automationId, chatId, since) {
222
+ const rows = await this.db.select({ actionResults: automationRuns.actionResults }).from(automationRuns).where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.automationId, automationId), eq(automationRuns.status, "completed"), gte(automationRuns.finishedAt, since), sql`${automationRuns.event}->'data'->>'chatId' = ${chatId}`));
223
+ const ids = [];
224
+ for (const row of rows) for (const result of row.actionResults) if (result.recipients) ids.push(...result.recipients);
225
+ return [...new Set(ids)];
226
+ }
161
227
  };
228
+ translateDbErrors(DrizzleAutomationsRepository);
229
+ translateDbErrors(DrizzleAutomationRunsRepository);
162
230
  //#endregion
163
231
  export { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository };
164
232
 
@@ -1 +1 @@
1
- {"version":3,"file":"automations.js","names":[],"sources":["../../src/repositories/automations.ts"],"sourcesContent":["// automations — Drizzle repositories (implement the core outbound ports).\n// Org-scoped: every method takes the domain `organizations.id` and constrains\n// its queries to that tenant. `Automation`/`AutomationRun` carry no `orgId`\n// column of their own in the domain type (mirrors the `Entitlement` shape) —\n// `AutomationRun` is the one exception, since it's the run's own `orgId`\n// field the service reads back off `handle`'s event.\nimport { and, asc, desc, eq, sql, type SQL } from 'drizzle-orm';\nimport type { DbExecutor } from '../client.js';\nimport type {\n Automation,\n AutomationRun,\n AutomationRunsQuery,\n AutomationRunsRepository,\n AutomationsRepository,\n CreateAutomationInput,\n NewAutomationRun,\n Page,\n UpdateAutomationInput,\n} from '@headless-lms/core/automations';\nimport { automations, automationRuns } from '../schema/index.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\nconst automationSelection = {\n orgId: automations.orgId,\n id: automations.id,\n name: automations.name,\n description: automations.description,\n trigger: automations.trigger,\n actions: automations.actions,\n enabled: automations.enabled,\n createdAt: automations.createdAt,\n updatedAt: automations.updatedAt,\n};\n\ninterface AutomationRow {\n orgId: string;\n id: string;\n name: string;\n description: string | null;\n trigger: string;\n actions: Automation['actions'];\n enabled: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomation(row: AutomationRow): Automation {\n return {\n orgId: row.orgId,\n id: row.id,\n name: row.name,\n description: row.description,\n trigger: row.trigger,\n actions: row.actions,\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleAutomationsRepository implements AutomationsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, input: CreateAutomationInput): Promise<Automation> {\n const [inserted] = await this.db\n .insert(automations)\n .values({\n orgId,\n name: input.name,\n description: input.description,\n trigger: input.trigger,\n actions: input.actions,\n })\n .returning({ id: automations.id });\n if (!inserted) {\n throw new Error('failed to insert automation');\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error('failed to load created automation');\n }\n return created;\n }\n\n async update(orgId: string, id: string, patch: UpdateAutomationInput): Promise<Automation | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.trigger !== undefined) {\n set.trigger = patch.trigger;\n }\n if (patch.actions !== undefined) {\n set.actions = patch.actions;\n }\n if (patch.enabled !== undefined) {\n set.enabled = patch.enabled;\n }\n\n const [updated] = await this.db\n .update(automations)\n .set(set)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning({ id: automations.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<Automation | null> {\n const [deleted] = await this.db\n .delete(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning(automationSelection);\n return deleted ? toAutomation(deleted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<Automation | null> {\n const [row] = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .limit(1);\n return row ? toAutomation(row) : null;\n }\n\n async listByOrg(orgId: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(eq(automations.orgId, orgId))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n\n async listByTrigger(orgId: string, trigger: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.trigger, trigger)))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n}\n\nconst runSelection = {\n id: automationRuns.id,\n orgId: automationRuns.orgId,\n automationId: automationRuns.automationId,\n trigger: automationRuns.trigger,\n eventId: automationRuns.eventId,\n event: automationRuns.event,\n status: automationRuns.status,\n actionResults: automationRuns.actionResults,\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n};\n\ninterface RunRow {\n id: string;\n orgId: string;\n automationId: string;\n trigger: string;\n eventId: string;\n event: AutomationRun['event'];\n status: AutomationRun['status'];\n actionResults: AutomationRun['actionResults'];\n startedAt: Date;\n finishedAt: Date | null;\n}\n\nfunction toAutomationRun(row: RunRow): AutomationRun {\n return {\n id: row.id,\n orgId: row.orgId,\n automationId: row.automationId,\n trigger: row.trigger,\n eventId: row.eventId,\n event: row.event,\n status: row.status,\n actionResults: row.actionResults,\n startedAt: row.startedAt,\n finishedAt: row.finishedAt,\n };\n}\n\n// Sortable columns by the client-facing field name. `-field` = desc.\nconst sortColumns = {\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n status: automationRuns.status,\n} as const;\n\nexport class DrizzleAutomationRunsRepository implements AutomationRunsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null> {\n const [inserted] = await this.db\n .insert(automationRuns)\n .values({\n orgId,\n automationId: run.automationId,\n trigger: run.trigger,\n eventId: run.event.id,\n event: run.event,\n status: run.status,\n actionResults: run.actionResults,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt,\n })\n // A redelivered trigger event hits the unique (org, automation, event) index; no row comes back.\n .onConflictDoNothing({\n target: [automationRuns.orgId, automationRuns.automationId, automationRuns.eventId],\n })\n .returning(runSelection);\n return inserted ? toAutomationRun(inserted) : null;\n }\n\n async recordOutcome(\n orgId: string,\n id: string,\n outcome: {\n status: AutomationRun['status'];\n actionResults: AutomationRun['actionResults'];\n finishedAt: Date;\n },\n ): Promise<AutomationRun | null> {\n const [updated] = await this.db\n .update(automationRuns)\n .set({\n status: outcome.status,\n actionResults: outcome.actionResults,\n finishedAt: outcome.finishedAt,\n })\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .returning(runSelection);\n return updated ? toAutomationRun(updated) : null;\n }\n\n async list(orgId: string, automationId: string, query: AutomationRunsQuery): Promise<Page<AutomationRun>> {\n const conditions: SQL[] = [\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.automationId, automationId),\n ];\n if (query.status) {\n conditions.push(eq(automationRuns.status, query.status));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently started first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? automationRuns.startedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(automationRuns.startedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(automationRuns)\n .where(where);\n\n return {\n rows: rows.map(toAutomationRun),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"automations.js","names":[],"sources":["../../src/repositories/automations.ts"],"sourcesContent":["// automations — Drizzle repositories (implement the core outbound ports).\n// Org-scoped: every method takes the domain `organizations.id` and constrains\n// its queries to that tenant. `Automation`/`AutomationRun` carry no `orgId`\n// column of their own in the domain type (mirrors the `Entitlement` shape) —\n// `AutomationRun` is the one exception, since it's the run's own `orgId`\n// field the service reads back off `handle`'s event.\nimport { and, arrayContains, asc, desc, eq, gte, ne, sql, type SQL } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type {\n Automation,\n AutomationKind,\n AutomationRun,\n AutomationRunsQuery,\n AutomationRunsRepository,\n AutomationsRepository,\n NewAutomation,\n NewAutomationRun,\n Page,\n UpdateAutomationInput,\n} from \"@headless-lms/core/automations\";\nimport type { ScheduledAutomation } from \"@headless-lms/core/types\";\nimport { automations, automationRuns } 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\nconst automationSelection = {\n orgId: automations.orgId,\n id: automations.id,\n name: automations.name,\n kind: automations.kind,\n description: automations.description,\n triggers: automations.triggers,\n actions: automations.actions,\n enabled: automations.enabled,\n filter: automations.filter,\n source: automations.source,\n createdAt: automations.createdAt,\n updatedAt: automations.updatedAt,\n};\n\ninterface AutomationRow {\n orgId: string;\n id: string;\n name: string;\n kind: AutomationKind;\n description: string | null;\n triggers: string[];\n actions: Automation[\"actions\"];\n enabled: boolean;\n filter: Automation[\"filter\"];\n source: Automation[\"source\"];\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomation(row: AutomationRow): Automation {\n return {\n orgId: row.orgId,\n id: row.id,\n name: row.name,\n kind: row.kind,\n description: row.description,\n triggers: row.triggers,\n actions: row.actions,\n enabled: row.enabled,\n filter: row.filter,\n source: row.source,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleAutomationsRepository implements AutomationsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, input: NewAutomation): Promise<Automation> {\n const [inserted] = await this.db\n .insert(automations)\n .values({\n orgId,\n name: input.name,\n kind: input.kind,\n description: input.description,\n triggers: input.triggers,\n actions: input.actions,\n filter: input.filter ?? null,\n source: input.source ?? \"user\",\n })\n .returning({ id: automations.id });\n if (!inserted) {\n throw new Error(\"failed to insert automation\");\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created automation\");\n }\n return created;\n }\n\n async insertSeed(orgId: string, input: NewAutomation): Promise<Automation | null> {\n const [inserted] = await this.db\n .insert(automations)\n .values({\n orgId,\n name: input.name,\n kind: input.kind,\n description: input.description,\n triggers: input.triggers,\n actions: input.actions,\n filter: input.filter ?? null,\n source: \"system\",\n })\n .onConflictDoNothing()\n .returning({ id: automations.id });\n return inserted ? this.findById(orgId, inserted.id) : null;\n }\n\n async update(\n orgId: string,\n id: string,\n patch: UpdateAutomationInput,\n ): Promise<Automation | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.triggers !== undefined) {\n set.triggers = patch.triggers;\n }\n if (patch.actions !== undefined) {\n set.actions = patch.actions;\n }\n if (patch.enabled !== undefined) {\n set.enabled = patch.enabled;\n }\n if (patch.filter !== undefined) {\n set.filter = patch.filter;\n }\n\n const [updated] = await this.db\n .update(automations)\n .set(set)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning({ id: automations.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<Automation | null> {\n const [deleted] = await this.db\n .delete(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .returning(automationSelection);\n return deleted ? toAutomation(deleted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<Automation | null> {\n const [row] = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), eq(automations.id, id)))\n .limit(1);\n return row ? toAutomation(row) : null;\n }\n\n async listByOrg(orgId: string, kind?: AutomationKind): Promise<Automation[]> {\n const conditions: SQL[] = [eq(automations.orgId, orgId)];\n if (kind) {\n conditions.push(eq(automations.kind, kind));\n }\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(...conditions))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n\n async listByTrigger(orgId: string, trigger: string): Promise<Automation[]> {\n const rows = await this.db\n .select(automationSelection)\n .from(automations)\n .where(and(eq(automations.orgId, orgId), arrayContains(automations.triggers, [trigger])))\n .orderBy(desc(automations.createdAt));\n return rows.map(toAutomation);\n }\n\n async listSchedules(): Promise<ScheduledAutomation[]> {\n const rows = await this.db\n .select({\n orgId: automations.orgId,\n id: automations.id,\n triggers: automations.triggers,\n })\n .from(automations)\n .where(\n and(\n eq(automations.enabled, true),\n sql`exists (select 1 from unnest(${automations.triggers}) as t(trigger) where t.trigger like 'schedule:%')`,\n ),\n );\n const schedules: ScheduledAutomation[] = [];\n for (const row of rows) {\n for (const trigger of row.triggers) {\n if (!trigger.startsWith(\"schedule:\")) {\n continue;\n }\n schedules.push({\n orgId: row.orgId,\n automationId: row.id,\n cron: trigger.slice(\"schedule:\".length),\n });\n }\n }\n return schedules;\n }\n}\n\nconst runSelection = {\n id: automationRuns.id,\n orgId: automationRuns.orgId,\n automationId: automationRuns.automationId,\n trigger: automationRuns.trigger,\n eventId: automationRuns.eventId,\n event: automationRuns.event,\n status: automationRuns.status,\n actionResults: automationRuns.actionResults,\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n createdAt: automationRuns.createdAt,\n updatedAt: automationRuns.updatedAt,\n};\n\ninterface RunRow {\n id: string;\n orgId: string;\n automationId: string;\n trigger: string;\n eventId: string;\n event: AutomationRun[\"event\"];\n status: AutomationRun[\"status\"];\n actionResults: AutomationRun[\"actionResults\"];\n startedAt: Date;\n finishedAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nfunction toAutomationRun(row: RunRow): AutomationRun {\n return {\n id: row.id,\n orgId: row.orgId,\n automationId: row.automationId,\n trigger: row.trigger,\n eventId: row.eventId,\n event: row.event,\n status: row.status,\n actionResults: row.actionResults,\n startedAt: row.startedAt,\n finishedAt: row.finishedAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n// Sortable columns by the client-facing field name. `-field` = desc.\nconst sortColumns = {\n startedAt: automationRuns.startedAt,\n finishedAt: automationRuns.finishedAt,\n status: automationRuns.status,\n} as const;\n\nexport class DrizzleAutomationRunsRepository implements AutomationRunsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, run: NewAutomationRun): Promise<AutomationRun | null> {\n const [inserted] = await this.db\n .insert(automationRuns)\n .values({\n orgId,\n automationId: run.automationId,\n trigger: run.trigger,\n eventId: run.event.id,\n event: run.event,\n status: run.status,\n actionResults: run.actionResults,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt,\n })\n // A redelivered trigger event hits the unique (org, automation, event) index; no row comes back.\n .onConflictDoNothing({\n target: [automationRuns.orgId, automationRuns.automationId, automationRuns.eventId],\n })\n .returning(runSelection);\n return inserted ? toAutomationRun(inserted) : null;\n }\n\n async findById(orgId: string, id: string): Promise<AutomationRun | null> {\n const [row] = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .limit(1);\n return row ? toAutomationRun(row) : null;\n }\n\n async restart(orgId: string, id: string, startedAt: Date): Promise<AutomationRun | null> {\n const [restarted] = await this.db\n .update(automationRuns)\n .set({ status: \"running\", actionResults: [], startedAt, finishedAt: null })\n .where(\n and(\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.id, id),\n ne(automationRuns.status, \"running\"),\n ),\n )\n .returning(runSelection);\n return restarted ? toAutomationRun(restarted) : null;\n }\n\n async recordOutcome(\n orgId: string,\n id: string,\n outcome: {\n status: AutomationRun[\"status\"];\n actionResults: AutomationRun[\"actionResults\"];\n finishedAt: Date;\n },\n ): Promise<AutomationRun | null> {\n const [updated] = await this.db\n .update(automationRuns)\n .set({\n status: outcome.status,\n actionResults: outcome.actionResults,\n finishedAt: outcome.finishedAt,\n })\n .where(and(eq(automationRuns.orgId, orgId), eq(automationRuns.id, id)))\n .returning(runSelection);\n return updated ? toAutomationRun(updated) : null;\n }\n\n async list(\n orgId: string,\n automationId: string,\n query: AutomationRunsQuery,\n ): Promise<Page<AutomationRun>> {\n const conditions: SQL[] = [\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.automationId, automationId),\n ];\n if (query.status) {\n conditions.push(eq(automationRuns.status, query.status));\n }\n const where = and(...conditions);\n\n // Sort: `-field` for descending; default to most-recently started first.\n let orderBy: SQL;\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const field = (isDesc ? query.sort.slice(1) : query.sort) as keyof typeof sortColumns;\n const col = sortColumns[field] ?? automationRuns.startedAt;\n orderBy = isDesc ? desc(col) : asc(col);\n } else {\n orderBy = desc(automationRuns.startedAt);\n }\n\n const offset = (query.page - 1) * query.pageSize;\n\n const rows = await this.db\n .select(runSelection)\n .from(automationRuns)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(automationRuns)\n .where(where);\n\n return {\n rows: rows.map(toAutomationRun),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async recentRecipients(\n orgId: string,\n automationId: string,\n chatId: string,\n since: Date,\n ): Promise<string[]> {\n const rows = await this.db\n .select({\n actionResults: automationRuns.actionResults,\n })\n .from(automationRuns)\n .where(\n and(\n eq(automationRuns.orgId, orgId),\n eq(automationRuns.automationId, automationId),\n eq(automationRuns.status, \"completed\"),\n gte(automationRuns.finishedAt, since),\n sql`${automationRuns.event}->'data'->>'chatId' = ${chatId}`,\n ),\n );\n const ids: string[] = [];\n for (const row of rows) {\n for (const result of row.actionResults) {\n if (result.recipients) {\n ids.push(...result.recipients);\n }\n }\n }\n return [...new Set(ids)];\n }\n}\ntranslateDbErrors(DrizzleAutomationsRepository);\ntranslateDbErrors(DrizzleAutomationRunsRepository);\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Chat, ChatUser, Logger, Message, MessageFlag, MessageFlagResolution, Page, ReactionEmoji } from "@headless-lms/core/types";
3
+ import { ChatListItem, ChatRepository, FeedQuery, MessageReactions, PageQuery } from "@headless-lms/core/discussion";
4
+ //#region src/repositories/chats.d.ts
5
+ declare class DrizzleChatRepository implements ChatRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ lockDirectPair(orgId: string, communityId: string, orgUserId: string, otherOrgUserId: string): Promise<void>;
10
+ findDirect(orgId: string, communityId: string, orgUserId: string, otherOrgUserId: string): Promise<Chat | null>;
11
+ insertChat(orgId: string, chat: Chat): Promise<Chat>;
12
+ updateChat(orgId: string, id: string, patch: Partial<Pick<Chat, "status" | "lockedAt" | "lockedBy">>): Promise<Chat | null>;
13
+ findChat(orgId: string, id: string): Promise<Chat | null>;
14
+ listForUser(orgId: string, communityId: string, orgUserId: string, query: PageQuery): Promise<Page<ChatListItem>>;
15
+ participants(orgId: string, chatId: string): Promise<ChatUser[]>;
16
+ join(orgId: string, user: ChatUser): Promise<void>;
17
+ markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;
18
+ setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;
19
+ setMuted(orgId: string, chatId: string, orgUserId: string, muted: boolean, mutedUntil: Date | null): Promise<void>;
20
+ insertMessage(orgId: string, message: Message): Promise<Message>;
21
+ updateMessage(orgId: string, id: string, patch: Partial<Pick<Message, "body" | "editedAt" | "deletedAt">>): Promise<Message | null>;
22
+ findMessage(orgId: string, id: string): Promise<Message | null>;
23
+ listMessages(orgId: string, chatId: string, query: Pick<FeedQuery, "page" | "pageSize">): Promise<Page<Message>>;
24
+ attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;
25
+ attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;
26
+ setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<void>;
27
+ reactionsOf(orgId: string, messageIds: string[], viewerId: string): Promise<Record<string, MessageReactions>>;
28
+ block(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
29
+ unblock(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
30
+ isBlocked(orgId: string, communityId: string, a: string, b: string): Promise<boolean>;
31
+ anyBlockedAmong(orgId: string, communityId: string, orgUserIds: string[]): Promise<boolean>;
32
+ blockedWith(orgId: string, communityId: string, orgUserId: string, otherIds: string[]): Promise<string[]>;
33
+ insertFlag(orgId: string, flag: MessageFlag): Promise<MessageFlag | null>;
34
+ findFlag(orgId: string, messageId: string, orgUserId: string): Promise<MessageFlag | null>;
35
+ listOpenFlags(orgId: string, communityId: string, limit?: number): Promise<MessageFlag[]>;
36
+ messagesByIds(orgId: string, ids: string[]): Promise<Message[]>;
37
+ chatSummaries(orgId: string, chatIds: string[]): Promise<{
38
+ chat: Chat;
39
+ participantIds: string[];
40
+ }[]>;
41
+ resolveOpenFlagsForChat(orgId: string, chatId: string, resolved: {
42
+ resolvedAt: Date;
43
+ resolvedBy: string;
44
+ resolution: MessageFlagResolution;
45
+ }): Promise<void>;
46
+ resolveFlags(orgId: string, messageId: string, resolved: {
47
+ resolvedAt: Date;
48
+ resolvedBy: string;
49
+ resolution: MessageFlagResolution;
50
+ }): Promise<void>;
51
+ private touch;
52
+ }
53
+ //#endregion
54
+ export { DrizzleChatRepository };
55
+ //# sourceMappingURL=chats.d.ts.map