@headless-lms/adapter-db 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/client.d.ts +8 -2
  2. package/dist/client.js +10 -2
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +5 -4
  5. package/dist/index.js +4 -3
  6. package/dist/repositories/admin.d.ts +12 -4
  7. package/dist/repositories/admin.js +164 -22
  8. package/dist/repositories/admin.js.map +1 -1
  9. package/dist/repositories/assets.d.ts +2 -1
  10. package/dist/repositories/assets.js +12 -1
  11. package/dist/repositories/assets.js.map +1 -1
  12. package/dist/repositories/automations.js +1 -1
  13. package/dist/repositories/chats.d.ts +13 -4
  14. package/dist/repositories/chats.js +164 -70
  15. package/dist/repositories/chats.js.map +1 -1
  16. package/dist/repositories/content.d.ts +22 -5
  17. package/dist/repositories/content.js +201 -40
  18. package/dist/repositories/content.js.map +1 -1
  19. package/dist/repositories/credentials.js +1 -1
  20. package/dist/repositories/devices.js +1 -1
  21. package/dist/repositories/digest-runs.js +1 -1
  22. package/dist/repositories/discussion.d.ts +1 -1
  23. package/dist/repositories/discussion.js +9 -3
  24. package/dist/repositories/discussion.js.map +1 -1
  25. package/dist/repositories/entitlements.d.ts +10 -10
  26. package/dist/repositories/entitlements.js +47 -56
  27. package/dist/repositories/entitlements.js.map +1 -1
  28. package/dist/repositories/event-reports.d.ts +16 -0
  29. package/dist/repositories/event-reports.js +199 -0
  30. package/dist/repositories/event-reports.js.map +1 -0
  31. package/dist/repositories/grant-covers.d.ts +4 -3
  32. package/dist/repositories/grant-covers.js +7 -4
  33. package/dist/repositories/grant-covers.js.map +1 -1
  34. package/dist/repositories/identity.js +1 -1
  35. package/dist/repositories/integrations.js +1 -1
  36. package/dist/repositories/member.d.ts +12 -4
  37. package/dist/repositories/member.js +19 -7
  38. package/dist/repositories/member.js.map +1 -1
  39. package/dist/repositories/members.d.ts +2 -5
  40. package/dist/repositories/members.js +39 -94
  41. package/dist/repositories/members.js.map +1 -1
  42. package/dist/repositories/mentions.js +1 -1
  43. package/dist/repositories/notification-settings.js +1 -1
  44. package/dist/repositories/notifications.js +2 -2
  45. package/dist/repositories/notifications.js.map +1 -1
  46. package/dist/repositories/org-user-profile.d.ts +0 -17
  47. package/dist/repositories/org-user-profile.js +0 -1
  48. package/dist/repositories/org-user-profile.js.map +1 -1
  49. package/dist/repositories/organizations.d.ts +4 -4
  50. package/dist/repositories/organizations.js +26 -46
  51. package/dist/repositories/organizations.js.map +1 -1
  52. package/dist/repositories/outbox.d.ts +5 -1
  53. package/dist/repositories/outbox.js +12 -3
  54. package/dist/repositories/outbox.js.map +1 -1
  55. package/dist/repositories/posts.js +1 -1
  56. package/dist/repositories/progress.js +1 -1
  57. package/dist/repositories/roles.d.ts +35 -0
  58. package/dist/repositories/roles.js +244 -0
  59. package/dist/repositories/roles.js.map +1 -0
  60. package/dist/repositories/settings.js +1 -1
  61. package/dist/repositories/subscriptions.js +1 -1
  62. package/dist/schema/assets.d.ts +17 -0
  63. package/dist/schema/assets.js +1 -0
  64. package/dist/schema/assets.js.map +1 -1
  65. package/dist/schema/automations.d.ts +3 -3
  66. package/dist/schema/automations.js +1 -1
  67. package/dist/schema/automations.js.map +1 -1
  68. package/dist/schema/content.d.ts +488 -3
  69. package/dist/schema/content.js +89 -4
  70. package/dist/schema/content.js.map +1 -1
  71. package/dist/schema/discussion.d.ts +17 -0
  72. package/dist/schema/discussion.js +4 -2
  73. package/dist/schema/discussion.js.map +1 -1
  74. package/dist/schema/index.d.ts +4 -3
  75. package/dist/schema/index.js +7 -2
  76. package/dist/schema/index.js.map +1 -1
  77. package/dist/schema/organizations.d.ts +24 -22
  78. package/dist/schema/organizations.js +3 -14
  79. package/dist/schema/organizations.js.map +1 -1
  80. package/dist/schema/roles.d.ts +369 -0
  81. package/dist/schema/roles.js +71 -0
  82. package/dist/schema/roles.js.map +1 -0
  83. package/drizzle/0017_configurable_roles.sql +38 -0
  84. package/drizzle/0018_messages_client_id.sql +2 -0
  85. package/drizzle/0019_events.sql +44 -0
  86. package/drizzle/0020_space_images.sql +9 -0
  87. package/drizzle/0021_org_metadata.sql +1 -0
  88. package/drizzle/meta/0017_snapshot.json +9518 -0
  89. package/drizzle/meta/0018_snapshot.json +9557 -0
  90. package/drizzle/meta/0019_snapshot.json +9919 -0
  91. package/drizzle/meta/0020_snapshot.json +10045 -0
  92. package/drizzle/meta/0021_snapshot.json +10052 -0
  93. package/drizzle/meta/_journal.json +35 -0
  94. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"members.js","names":[],"sources":["../../src/repositories/members.ts"],"sourcesContent":["// organizations members — Drizzle read repository. Reads the domain mirror of the\n// org's members (orgUsers) and pending invites, joined to the identity user for\n// display. Writes go through the auth provider (see adapters/auth/org-admin.ts).\nimport { and, eq, ne } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n isRole,\n STUDENT_ROLE,\n type Member,\n type MemberRecord,\n type MembersQuery,\n type MembersRepository,\n type Page,\n type TeamRole,\n} from \"@headless-lms/core/organizations\";\nimport { orgUsers, invites } from \"../schema/organizations.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\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 roleOf = (t: string): TeamRole => (isRole(t) && t !== STUDENT_ROLE ? t : \"instructor\");\n\nfunction toMember(r: MemberRecord): Member {\n return {\n id: r.id,\n firstName: r.firstName,\n lastName: r.lastName,\n email: r.email,\n image: r.image,\n role: r.role,\n status: r.status,\n joinedAt: r.joinedAt,\n invitedAt: r.invitedAt,\n };\n}\n\nexport class DrizzleMembersRepository implements MembersRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private async loadAll(orgId: string): Promise<MemberRecord[]> {\n // Learners share this table now, so the role filter is what keeps them off\n // the staff list. Name/email come from the identity user the row links to.\n const memberRows = await this.db\n .select({\n ...orgUserProfileColumns,\n role: orgUsers.role,\n joinedAt: orgUsers.createdAt,\n userExternalId: users.externalId,\n })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), ne(orgUsers.role, STUDENT_ROLE)));\n\n // Student invites live in the same table but belong to the students\n // surface, not the members list.\n const inviteRows = await this.db\n .select({\n id: invites.id,\n email: invites.email,\n role: invites.role,\n invitedAt: invites.createdAt,\n })\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n eq(invites.status, \"pending\"),\n ne(invites.role, STUDENT_ROLE),\n ),\n );\n\n const members: MemberRecord[] = memberRows.map((m) => ({\n id: m.id,\n firstName: m.firstName,\n lastName: m.lastName,\n email: m.email,\n image: m.image ?? null,\n role: roleOf(m.role),\n status: \"active\",\n joinedAt: m.joinedAt.toISOString(),\n invitedAt: null,\n kind: \"member\",\n userExternalId: m.userExternalId,\n inviteId: null,\n }));\n // A pending invite is an address and nothing else — the invites table\n // carries no names, so there is nobody to name until they accept.\n const invited: MemberRecord[] = inviteRows.map((i) => ({\n id: i.id,\n firstName: null,\n lastName: null,\n email: i.email,\n image: null,\n role: roleOf(i.role),\n status: \"invited\",\n joinedAt: null,\n invitedAt: i.invitedAt.toISOString(),\n kind: \"invite\",\n userExternalId: null,\n inviteId: i.id,\n }));\n return [...members, ...invited];\n }\n\n async list(orgId: string, query: MembersQuery): Promise<Page<Member>> {\n let rows = await this.loadAll(orgId);\n if (query.role) {\n rows = rows.filter((r) => r.role === query.role);\n }\n if (query.status) {\n rows = rows.filter((r) => r.status === query.status);\n }\n const q = query.search?.trim().toLowerCase();\n if (q) {\n rows = rows.filter((r) =>\n [r.firstName, r.lastName, r.email].some((v) => v?.toLowerCase().includes(q)),\n );\n }\n\n const sort = query.sort;\n const desc = sort?.startsWith(\"-\") ?? false;\n const key = (desc ? sort!.slice(1) : sort) as keyof Member | undefined;\n // Nulls last whichever way the column is sorted — an unaccepted invite has\n // no name and belongs at the bottom, not interleaved.\n const byName = (a: string | null, b: string | null): number =>\n a === b ? 0 : a === null ? 1 : b === null ? -1 : a.localeCompare(b);\n rows.sort((a, b) => {\n if (key === \"email\") {\n return desc ? -a.email.localeCompare(b.email) : a.email.localeCompare(b.email);\n }\n if (key === \"role\") {\n return desc ? -a.role.localeCompare(b.role) : a.role.localeCompare(b.role);\n }\n if (key === \"lastName\") {\n const cmp = byName(a.lastName, b.lastName) || byName(a.firstName, b.firstName);\n return desc ? -cmp : cmp;\n }\n const cmp = byName(a.firstName, b.firstName) || byName(a.lastName, b.lastName);\n return desc ? -cmp : cmp;\n });\n\n const total = rows.length;\n const start = (query.page - 1) * query.pageSize;\n return {\n rows: rows.slice(start, start + query.pageSize).map(toMember),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findByEmail(orgId: string, email: string): Promise<MemberRecord | null> {\n const all = await this.loadAll(orgId);\n return all.find((r) => r.email.toLowerCase() === email.toLowerCase()) ?? null;\n }\n\n async findById(orgId: string, id: string): Promise<MemberRecord | null> {\n const all = await this.loadAll(orgId);\n return all.find((r) => r.id === id) ?? null;\n }\n}\ntranslateDbErrors(DrizzleMembersRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"members.js","names":[],"sources":["../../src/repositories/members.ts"],"sourcesContent":["import { and, eq, inArray, isNotNull, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { MembersRepository, TeamRow } from \"@headless-lms/core/organizations\";\nimport type { ContentType } from \"@headless-lms/core/types\";\nimport { orgUsers, invites } from \"../schema/organizations.js\";\nimport { roleAssignments } from \"../schema/roles.js\";\nimport { users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\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\nexport class DrizzleMembersRepository implements MembersRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async teamRows(orgId: string, memberRoleKey: string): Promise<TeamRow[]> {\n const pendingInvite = this.db\n .select({\n email: invites.email,\n roleKey: invites.roleKey,\n invitedAt: invites.createdAt,\n })\n .from(invites)\n .where(and(eq(invites.orgId, orgId), eq(invites.status, \"pending\")))\n .as(\"pending_invite\");\n const rows = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n joinedAt: orgUsers.createdAt,\n inviteRoleKey: pendingInvite.roleKey,\n invitedAt: pendingInvite.invitedAt,\n })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .leftJoin(pendingInvite, sql`lower(${pendingInvite.email}) = lower(${users.email})`)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n or(\n sql`exists (select 1 from ${roleAssignments} ra where ra.org_id = ${orgUsers.orgId} and ra.org_user_id = ${orgUsers.id})`,\n and(\n eq(orgUsers.status, \"invited\"),\n isNotNull(pendingInvite.roleKey),\n sql`${pendingInvite.roleKey} <> ${memberRoleKey}`,\n ),\n ),\n ),\n );\n if (rows.length === 0) {\n return [];\n }\n const assignments = await this.db\n .select({\n orgUserId: roleAssignments.orgUserId,\n roleKey: roleAssignments.roleKey,\n contentId: roleAssignments.contentId,\n contentType: roleAssignments.contentType,\n })\n .from(roleAssignments)\n .where(\n and(\n eq(roleAssignments.orgId, orgId),\n inArray(\n roleAssignments.orgUserId,\n rows.map((row) => row.id),\n ),\n ),\n );\n return rows.map((row) => {\n const own = assignments.filter((a) => a.orgUserId === row.id);\n return {\n id: row.id,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email,\n image: row.image ?? null,\n status: row.status,\n joinedAt: row.joinedAt,\n invitedAt: row.invitedAt ?? null,\n orgRoleKey: own.find((a) => a.contentId === null)?.roleKey ?? null,\n inviteRoleKey: row.inviteRoleKey ?? null,\n items: own\n .filter((a) => a.contentId !== null && a.contentType !== null)\n .map((a) => ({\n contentId: a.contentId!,\n contentType: a.contentType as ContentType,\n roleKey: a.roleKey,\n })),\n };\n });\n }\n}\ntranslateDbErrors(DrizzleMembersRepository);\n"],"mappings":";;;;;;;;;AAaA,IAAa,2BAAb,MAAmE;CAE9C;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,SAAS,OAAe,eAA2C;EACvE,MAAM,gBAAgB,KAAK,GACxB,OAAO;GACN,OAAO,QAAQ;GACf,SAAS,QAAQ;GACjB,WAAW,QAAQ;EACrB,CAAC,CAAC,CACD,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,QAAQ,SAAS,CAAC,CAAC,CAAC,CACnE,GAAG,gBAAgB;EACtB,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO;GACN,GAAG;GACH,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,eAAe,cAAc;GAC7B,WAAW,cAAc;EAC3B,CAAC,CAAC,CACD,KAAK,QAAQ,CAAC,CACd,UAAU,OAAO,GAAG,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,CAC/C,SAAS,MAAM,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC,CAAC,CAC7C,SAAS,eAAe,GAAG,SAAS,cAAc,MAAM,YAAY,MAAM,MAAM,EAAE,CAAC,CACnF,MACC,IACE,GAAG,SAAS,OAAO,KAAK,GACxB,GACE,GAAG,yBAAyB,gBAAgB,wBAAwB,SAAS,MAAM,wBAAwB,SAAS,GAAG,IACvH,IACE,GAAG,SAAS,QAAQ,SAAS,GAC7B,UAAU,cAAc,OAAO,GAC/B,GAAG,GAAG,cAAc,QAAQ,MAAM,eACpC,CACF,CACF,CACF;EACF,IAAI,KAAK,WAAW,GAClB,OAAO,CAAC;EAEV,MAAM,cAAc,MAAM,KAAK,GAC5B,OAAO;GACN,WAAW,gBAAgB;GAC3B,SAAS,gBAAgB;GACzB,WAAW,gBAAgB;GAC3B,aAAa,gBAAgB;EAC/B,CAAC,CAAC,CACD,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,QACE,gBAAgB,WAChB,KAAK,KAAK,QAAQ,IAAI,EAAE,CAC1B,CACF,CACF;EACF,OAAO,KAAK,KAAK,QAAQ;GACvB,MAAM,MAAM,YAAY,QAAQ,MAAM,EAAE,cAAc,IAAI,EAAE;GAC5D,OAAO;IACL,IAAI,IAAI;IACR,WAAW,IAAI;IACf,UAAU,IAAI;IACd,OAAO,IAAI;IACX,OAAO,IAAI,SAAS;IACpB,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,WAAW,IAAI,aAAa;IAC5B,YAAY,IAAI,MAAM,MAAM,EAAE,cAAc,IAAI,CAAC,EAAE,WAAW;IAC9D,eAAe,IAAI,iBAAiB;IACpC,OAAO,IACJ,QAAQ,MAAM,EAAE,cAAc,QAAQ,EAAE,gBAAgB,IAAI,CAAC,CAC7D,KAAK,OAAO;KACX,WAAW,EAAE;KACb,aAAa,EAAE;KACf,SAAS,EAAE;IACb,EAAE;GACN;EACF,CAAC;CACH;AACF;AACA,kBAAkB,wBAAwB"}
@@ -1,7 +1,7 @@
1
1
  import { mentions } from "../schema/discussion.js";
2
2
  import "../schema/index.js";
3
- import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
4
3
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { and, asc, eq, inArray, notInArray } from "drizzle-orm";
5
5
  //#region src/repositories/mentions.ts
6
6
  var DrizzleMentionsRepository = class {
7
7
  db;
@@ -2,9 +2,9 @@ import { notificationSettings } from "../schema/identity.js";
2
2
  import { orgUsers } from "../schema/organizations.js";
3
3
  import "../schema/index.js";
4
4
  import { translateDbErrors } from "./pg-errors.js";
5
+ import { noopLogger } from "@headless-lms/core/shared/logger";
5
6
  import { and, eq, inArray, sql } from "drizzle-orm";
6
7
  import { genId } from "@headless-lms/core/shared/id";
7
- import { noopLogger } from "@headless-lms/core/shared/logger";
8
8
  //#region src/repositories/notification-settings.ts
9
9
  const ns = notificationSettings;
10
10
  const communityKey = sql`coalesce(${ns.communityId}, '')`;
@@ -23,7 +23,7 @@ function toStored(row) {
23
23
  category: row.category,
24
24
  communityId: row.communityId,
25
25
  spaceId: row.spaceId,
26
- actorOrgUserId: row.actorOrgUserId,
26
+ actorId: row.actorId,
27
27
  subject: ref(row.subjectType, row.subjectId),
28
28
  parent: ref(row.parentType, row.parentId),
29
29
  display: row.display,
@@ -79,7 +79,7 @@ var DrizzleNotificationsRepository = class {
79
79
  category: row.category,
80
80
  communityId: row.communityId,
81
81
  spaceId: row.spaceId,
82
- actorOrgUserId: row.actorOrgUserId,
82
+ actorId: row.actorId,
83
83
  subjectType: row.subject?.type ?? null,
84
84
  subjectId: row.subject?.id ?? null,
85
85
  parentType: row.parent?.type ?? null,
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import {\n and,\n arrayOverlaps,\n desc,\n eq,\n inArray,\n isNotNull,\n isNull,\n or,\n sql,\n type SQL,\n} from \"drizzle-orm\";\nimport type {\n ListedNotification,\n NewDelivery,\n NewNotification,\n NotificationPage,\n NotificationsRepository,\n UnreadCounts,\n} from \"@headless-lms/core/automations\";\nimport type {\n Delivery,\n NotificationFilter,\n StoredNotification,\n SubjectRef,\n} from \"@headless-lms/core/types\";\nimport type { DbExecutor } from \"../client.js\";\nimport { notificationDeliveries, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst n = notifications;\nconst d = notificationDeliveries;\n\ntype NotificationRow = typeof notifications.$inferSelect;\ntype DeliveryRow = typeof notificationDeliveries.$inferSelect;\n\nfunction ref(type: string | null, id: string | null): SubjectRef | null {\n return type && id ? { type: type as SubjectRef[\"type\"], id } : null;\n}\n\nfunction toStored(row: NotificationRow): StoredNotification {\n return {\n id: row.id,\n orgId: row.orgId,\n userId: row.userId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subject: ref(row.subjectType, row.subjectId),\n parent: ref(row.parentType, row.parentId),\n display: row.display,\n createdAt: row.createdAt,\n };\n}\n\nfunction toDelivery(row: DeliveryRow): Delivery {\n return {\n id: row.id,\n notificationId: row.notificationId,\n channel: row.channel,\n status: row.status,\n lastError: row.lastError,\n createdAt: row.createdAt,\n sentAt: row.sentAt,\n };\n}\n\nfunction listed(orgId: string, userId: string, filter: NotificationFilter): SQL[] {\n const conditions: SQL[] = [eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true)];\n if (filter.scope === \"org\") {\n conditions.push(isNull(n.communityId));\n }\n if (filter.scope === \"community\") {\n conditions.push(isNotNull(n.communityId));\n }\n if (filter.communityId !== undefined) {\n const community = eq(n.communityId, filter.communityId);\n conditions.push(filter.includeOrg ? or(community, isNull(n.communityId))! : community);\n }\n if (filter.category && filter.category.length > 0) {\n conditions.push(inArray(n.category, filter.category));\n }\n if (filter.reason && filter.reason.length > 0) {\n conditions.push(arrayOverlaps(n.reasons, filter.reason));\n }\n if (filter.type && filter.type.length > 0) {\n conditions.push(inArray(n.type, filter.type));\n }\n if (filter.unread === true) {\n conditions.push(isNull(n.readAt));\n }\n return conditions;\n}\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: DbExecutor) {}\n\n async insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n const inserted = await tx\n .insert(n)\n .values(\n rows.map((row) => ({\n orgId,\n id: row.id,\n userId: row.userId,\n sourceEventId: row.sourceEventId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subjectType: row.subject?.type ?? null,\n subjectId: row.subject?.id ?? null,\n parentType: row.parent?.type ?? null,\n parentId: row.parent?.id ?? null,\n display: row.display,\n inApp: row.inApp,\n createdAt: row.createdAt,\n })),\n )\n .onConflictDoNothing()\n .returning({ id: n.id });\n const kept = new Set(inserted.map((row) => row.id));\n const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));\n if (pending.length === 0) {\n return;\n }\n await tx\n .insert(d)\n .values(\n pending.map((delivery) => ({\n orgId,\n id: delivery.id,\n notificationId: delivery.notificationId,\n channel: delivery.channel,\n status: \"pending\" as const,\n })),\n )\n .onConflictDoNothing();\n });\n }\n\n async pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{ id: string }[]> {\n return this.db\n .select({ id: d.id })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, \"pending\")))\n .orderBy(d.id);\n }\n\n async findDelivery(\n orgId: string,\n deliveryId: string,\n ): Promise<{ delivery: Delivery; notification: StoredNotification } | null> {\n const [row] = await this.db\n .select({ delivery: d, notification: n })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId)))\n .limit(1);\n return row\n ? { delivery: toDelivery(row.delivery), notification: toStored(row.notification) }\n : null;\n }\n\n async markSent(orgId: string, deliveryId: string, at: Date): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"sent\", sentAt: at })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async recordError(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async markFailed(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"failed\", lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async page(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n ): Promise<NotificationPage> {\n const where = and(...listed(orgId, userId, filter));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(n)\n .where(where)\n .orderBy(desc(n.createdAt), desc(n.id))\n .limit(page.pageSize)\n .offset((page.page - 1) * page.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${n.readAt} is null))::int`,\n })\n .from(n)\n .where(where),\n ]);\n return {\n items: rows.map((row) => ({ ...toStored(row), readAt: row.readAt })),\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async find(\n orgId: string,\n userId: string,\n notificationId: string,\n ): Promise<ListedNotification | null> {\n const [row] = await this.db\n .select()\n .from(n)\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .limit(1);\n return row ? { ...toStored(row), readAt: row.readAt } : null;\n }\n\n async unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts> {\n const unread = and(\n eq(n.orgId, orgId),\n eq(n.userId, userId),\n eq(n.inApp, true),\n isNull(n.readAt),\n );\n const [org, communities] = await Promise.all([\n this.db\n .select({ unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, isNull(n.communityId))),\n communityIds.length === 0\n ? Promise.resolve([])\n : this.db\n .select({ communityId: n.communityId, unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, inArray(n.communityId, communityIds)))\n .groupBy(n.communityId),\n ]);\n return {\n org: Number(org[0]?.unread ?? 0),\n communities: communities.flatMap((row) =>\n row.communityId ? [{ communityId: row.communityId, unread: Number(row.unread) }] : [],\n ),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<boolean> {\n const rows = await this.db\n .update(n)\n .set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .returning({ id: n.id });\n return rows.length > 0;\n }\n\n async markAllRead(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n at: Date,\n ): Promise<number> {\n const rows = await this.db\n .update(n)\n .set({ readAt: at })\n .where(and(...listed(orgId, userId, filter), isNull(n.readAt)))\n .returning({ id: n.id });\n return rows.length;\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import {\n and,\n arrayOverlaps,\n desc,\n eq,\n inArray,\n isNotNull,\n isNull,\n or,\n sql,\n type SQL,\n} from \"drizzle-orm\";\nimport type {\n ListedNotification,\n NewDelivery,\n NewNotification,\n NotificationPage,\n NotificationsRepository,\n UnreadCounts,\n} from \"@headless-lms/core/automations\";\nimport type {\n Delivery,\n NotificationFilter,\n StoredNotification,\n SubjectRef,\n} from \"@headless-lms/core/types\";\nimport type { DbExecutor } from \"../client.js\";\nimport { notificationDeliveries, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst n = notifications;\nconst d = notificationDeliveries;\n\ntype NotificationRow = typeof notifications.$inferSelect;\ntype DeliveryRow = typeof notificationDeliveries.$inferSelect;\n\nfunction ref(type: string | null, id: string | null): SubjectRef | null {\n return type && id ? { type: type as SubjectRef[\"type\"], id } : null;\n}\n\nfunction toStored(row: NotificationRow): StoredNotification {\n return {\n id: row.id,\n orgId: row.orgId,\n userId: row.userId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorId: row.actorId,\n subject: ref(row.subjectType, row.subjectId),\n parent: ref(row.parentType, row.parentId),\n display: row.display,\n createdAt: row.createdAt,\n };\n}\n\nfunction toDelivery(row: DeliveryRow): Delivery {\n return {\n id: row.id,\n notificationId: row.notificationId,\n channel: row.channel,\n status: row.status,\n lastError: row.lastError,\n createdAt: row.createdAt,\n sentAt: row.sentAt,\n };\n}\n\nfunction listed(orgId: string, userId: string, filter: NotificationFilter): SQL[] {\n const conditions: SQL[] = [eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true)];\n if (filter.scope === \"org\") {\n conditions.push(isNull(n.communityId));\n }\n if (filter.scope === \"community\") {\n conditions.push(isNotNull(n.communityId));\n }\n if (filter.communityId !== undefined) {\n const community = eq(n.communityId, filter.communityId);\n conditions.push(filter.includeOrg ? or(community, isNull(n.communityId))! : community);\n }\n if (filter.category && filter.category.length > 0) {\n conditions.push(inArray(n.category, filter.category));\n }\n if (filter.reason && filter.reason.length > 0) {\n conditions.push(arrayOverlaps(n.reasons, filter.reason));\n }\n if (filter.type && filter.type.length > 0) {\n conditions.push(inArray(n.type, filter.type));\n }\n if (filter.unread === true) {\n conditions.push(isNull(n.readAt));\n }\n return conditions;\n}\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: DbExecutor) {}\n\n async insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n const inserted = await tx\n .insert(n)\n .values(\n rows.map((row) => ({\n orgId,\n id: row.id,\n userId: row.userId,\n sourceEventId: row.sourceEventId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorId: row.actorId,\n subjectType: row.subject?.type ?? null,\n subjectId: row.subject?.id ?? null,\n parentType: row.parent?.type ?? null,\n parentId: row.parent?.id ?? null,\n display: row.display,\n inApp: row.inApp,\n createdAt: row.createdAt,\n })),\n )\n .onConflictDoNothing()\n .returning({ id: n.id });\n const kept = new Set(inserted.map((row) => row.id));\n const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));\n if (pending.length === 0) {\n return;\n }\n await tx\n .insert(d)\n .values(\n pending.map((delivery) => ({\n orgId,\n id: delivery.id,\n notificationId: delivery.notificationId,\n channel: delivery.channel,\n status: \"pending\" as const,\n })),\n )\n .onConflictDoNothing();\n });\n }\n\n async pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{ id: string }[]> {\n return this.db\n .select({ id: d.id })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, \"pending\")))\n .orderBy(d.id);\n }\n\n async findDelivery(\n orgId: string,\n deliveryId: string,\n ): Promise<{ delivery: Delivery; notification: StoredNotification } | null> {\n const [row] = await this.db\n .select({ delivery: d, notification: n })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId)))\n .limit(1);\n return row\n ? { delivery: toDelivery(row.delivery), notification: toStored(row.notification) }\n : null;\n }\n\n async markSent(orgId: string, deliveryId: string, at: Date): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"sent\", sentAt: at })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async recordError(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async markFailed(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"failed\", lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async page(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n ): Promise<NotificationPage> {\n const where = and(...listed(orgId, userId, filter));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(n)\n .where(where)\n .orderBy(desc(n.createdAt), desc(n.id))\n .limit(page.pageSize)\n .offset((page.page - 1) * page.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${n.readAt} is null))::int`,\n })\n .from(n)\n .where(where),\n ]);\n return {\n items: rows.map((row) => ({ ...toStored(row), readAt: row.readAt })),\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async find(\n orgId: string,\n userId: string,\n notificationId: string,\n ): Promise<ListedNotification | null> {\n const [row] = await this.db\n .select()\n .from(n)\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .limit(1);\n return row ? { ...toStored(row), readAt: row.readAt } : null;\n }\n\n async unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts> {\n const unread = and(\n eq(n.orgId, orgId),\n eq(n.userId, userId),\n eq(n.inApp, true),\n isNull(n.readAt),\n );\n const [org, communities] = await Promise.all([\n this.db\n .select({ unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, isNull(n.communityId))),\n communityIds.length === 0\n ? Promise.resolve([])\n : this.db\n .select({ communityId: n.communityId, unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, inArray(n.communityId, communityIds)))\n .groupBy(n.communityId),\n ]);\n return {\n org: Number(org[0]?.unread ?? 0),\n communities: communities.flatMap((row) =>\n row.communityId ? [{ communityId: row.communityId, unread: Number(row.unread) }] : [],\n ),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<boolean> {\n const rows = await this.db\n .update(n)\n .set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .returning({ id: n.id });\n return rows.length > 0;\n }\n\n async markAllRead(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n at: Date,\n ): Promise<number> {\n const rows = await this.db\n .update(n)\n .set({ readAt: at })\n .where(and(...listed(orgId, userId, filter), isNull(n.readAt)))\n .returning({ id: n.id });\n return rows.length;\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":"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"}
@@ -88,23 +88,6 @@ declare const orgUserProfileColumns: {
88
88
  }, {}, {}>;
89
89
  };
90
90
  declare const authorProfileColumns: {
91
- role: import("drizzle-orm/pg-core").PgColumn<{
92
- name: "role";
93
- tableName: "org_users";
94
- dataType: "string";
95
- columnType: "PgText";
96
- data: "student" | "owner" | "admin" | "instructor" | "moderator";
97
- driverParam: string;
98
- notNull: true;
99
- hasDefault: false;
100
- isPrimaryKey: false;
101
- isAutoincrement: false;
102
- hasRuntimeDefault: false;
103
- enumValues: ["owner", "admin", "instructor", "moderator", "student"];
104
- baseColumn: never;
105
- identity: undefined;
106
- generated: undefined;
107
- }, {}, {}>;
108
91
  displayName: import("drizzle-orm/pg-core").PgColumn<{
109
92
  name: "display_name";
110
93
  tableName: "user_profiles";
@@ -13,7 +13,6 @@ const orgUserProfileColumns = {
13
13
  };
14
14
  const authorProfileColumns = {
15
15
  ...orgUserProfileColumns,
16
- role: orgUsers.role,
17
16
  displayName: userProfiles.displayName,
18
17
  headline: userProfiles.headline,
19
18
  imageAssetOrgId: userProfiles.imageAssetOrgId,
@@ -1 +1 @@
1
- {"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { sql } from \"drizzle-orm\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\n\n/** Spread into a `.select({ ... })` to get the profile columns. */\nexport const orgUserProfileColumns = {\n id: orgUsers.id,\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n image: user.image,\n};\n\nexport const authorProfileColumns = {\n ...orgUserProfileColumns,\n role: orgUsers.role,\n displayName: userProfiles.displayName,\n headline: userProfiles.headline,\n imageAssetOrgId: userProfiles.imageAssetOrgId,\n imageAssetId: userProfiles.imageAssetId,\n};\n\n// A person is shared across organizations. Until an invited student accepts,\n// the inviting org sees the name and avatar only when the profile is its own:\n// no account yet and no other organization.\nconst profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (\n select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}\n)))`;\n\nexport const studentFirstName = sql<\n string | null\n>`case when ${profileVisible} then ${users.firstName} end`;\nexport const studentLastName = sql<\n string | null\n>`case when ${profileVisible} then ${users.lastName} end`;\n\nexport const studentProfileColumns = {\n id: orgUsers.id,\n firstName: studentFirstName,\n lastName: studentLastName,\n email: users.email,\n image: sql<string | null>`case when ${profileVisible} then ${user.image} end`,\n};\n"],"mappings":";;;;;;AAoBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd;AAEA,MAAa,uBAAuB;CAClC,GAAG;CACH,MAAM,SAAS;CACf,aAAa,aAAa;CAC1B,UAAU,aAAa;CACvB,iBAAiB,aAAa;CAC9B,cAAc,aAAa;AAC7B;AAKA,MAAM,iBAAiB,GAAG,IAAI,SAAS,OAAO,oBAAoB,MAAM,WAAW;wDAC3B,MAAM,GAAG,uBAAuB,SAAS,MAAM;;AAGvG,MAAa,mBAAmB,GAE/B,aAAa,eAAe,QAAQ,MAAM,UAAU;AACrD,MAAa,kBAAkB,GAE9B,aAAa,eAAe,QAAQ,MAAM,SAAS;AAEpD,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW;CACX,UAAU;CACV,OAAO,MAAM;CACb,OAAO,GAAkB,aAAa,eAAe,QAAQ,KAAK,MAAM;AAC1E"}
1
+ {"version":3,"file":"org-user-profile.js","names":[],"sources":["../../src/repositories/org-user-profile.ts"],"sourcesContent":["// The one display join for an org user's profile.\n//\n// The names and email live on the identity `users` row the org_users row links\n// to; the avatar lives on the auth engine's mirrored `user` table, reached\n// through that same identity row. The identity join is INNER (user_id is NOT\n// NULL); the auth join is LEFT, since only the avatar depends on it.\n//\n// Sanctioned by .eslintrc.cjs — \"db repositories read the auth adapter's\n// mirrored `user` table for display joins\".\n//\n// No `joinOrgUserProfile` helper: Drizzle's query builder type narrows on every\n// chained call, so a generic wrapper around `.innerJoin(...).leftJoin(...)`\n// can't be typed without widening it to `unknown` — each repository chains the\n// two joins inline instead.\nimport { sql } from \"drizzle-orm\";\nimport { orgUsers } from \"../schema/organizations.js\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { user } from \"../schema/better-auth.js\";\n\n/** Spread into a `.select({ ... })` to get the profile columns. */\nexport const orgUserProfileColumns = {\n id: orgUsers.id,\n firstName: users.firstName,\n lastName: users.lastName,\n email: users.email,\n image: user.image,\n};\n\nexport const authorProfileColumns = {\n ...orgUserProfileColumns,\n displayName: userProfiles.displayName,\n headline: userProfiles.headline,\n imageAssetOrgId: userProfiles.imageAssetOrgId,\n imageAssetId: userProfiles.imageAssetId,\n};\n\n// A person is shared across organizations. Until an invited student accepts,\n// the inviting org sees the name and avatar only when the profile is its own:\n// no account yet and no other organization.\nconst profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (\n select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}\n)))`;\n\nexport const studentFirstName = sql<\n string | null\n>`case when ${profileVisible} then ${users.firstName} end`;\nexport const studentLastName = sql<\n string | null\n>`case when ${profileVisible} then ${users.lastName} end`;\n\nexport const studentProfileColumns = {\n id: orgUsers.id,\n firstName: studentFirstName,\n lastName: studentLastName,\n email: users.email,\n image: sql<string | null>`case when ${profileVisible} then ${user.image} end`,\n};\n"],"mappings":";;;;;;AAoBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd;AAEA,MAAa,uBAAuB;CAClC,GAAG;CACH,aAAa,aAAa;CAC1B,UAAU,aAAa;CACvB,iBAAiB,aAAa;CAC9B,cAAc,aAAa;AAC7B;AAKA,MAAM,iBAAiB,GAAG,IAAI,SAAS,OAAO,oBAAoB,MAAM,WAAW;wDAC3B,MAAM,GAAG,uBAAuB,SAAS,MAAM;;AAGvG,MAAa,mBAAmB,GAE/B,aAAa,eAAe,QAAQ,MAAM,UAAU;AACrD,MAAa,kBAAkB,GAE9B,aAAa,eAAe,QAAQ,MAAM,SAAS;AAEpD,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW;CACX,UAAU;CACV,OAAO,MAAM;CACb,OAAO,GAAkB,aAAa,eAAe,QAAQ,KAAK,MAAM;AAC1E"}
@@ -1,5 +1,5 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, Role, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
2
+ import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationMetadata, OrganizationsRepository, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
3
3
  import { Logger } from "@headless-lms/core/shared/ports";
4
4
  //#region src/repositories/organizations.d.ts
5
5
  declare class DrizzleOrganizationsRepository implements OrganizationsRepository {
@@ -12,20 +12,20 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
12
12
  findById(id: string): Promise<Organization | null>;
13
13
  listIds(): Promise<string[]>;
14
14
  findByExternalId(externalId: string): Promise<Organization | null>;
15
+ setMetadata(id: string, metadata: OrganizationMetadata): Promise<Organization>;
15
16
  findBySlug(slug: string): Promise<Organization | null>;
16
17
  upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite>;
17
18
  setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null>;
19
+ setInviteRole(orgId: string, id: string, roleKey: string, invitedBy: string): Promise<Invite | null>;
18
20
  findInviteByTokenHash(tokenHash: string): Promise<Invite | null>;
19
21
  findPendingInvite(orgId: string, email: string): Promise<Invite | null>;
20
- setInviteToken(orgId: string, id: string, tokenHash: string, expiresAt: Date): Promise<Invite | null>;
22
+ setInviteToken(orgId: string, id: string, tokenHash: string, expiresAt: Date, invitedBy: string): Promise<Invite | null>;
21
23
  findOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
22
24
  findOrgUsersByUser(userId: string): Promise<OrgUser[]>;
23
25
  findOrgUserById(orgId: string, id: string): Promise<OrgUser | null>;
24
26
  findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]>;
25
- findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]>;
26
27
  createOrgUser(input: CreateOrgUserInput): Promise<OrgUser>;
27
28
  updateOrgUser(orgId: string, id: string, patch: UpdateOrgUserInput): Promise<OrgUser | null>;
28
- findStudentOrgUsers(userId: string): Promise<OrgUser[]>;
29
29
  deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null>;
30
30
  }
31
31
  //#endregion
@@ -1,9 +1,8 @@
1
1
  import { invites, orgUsers, organizations } from "../schema/organizations.js";
2
2
  import { isUniqueViolation, pgErrorFields, translateDbErrors } from "./pg-errors.js";
3
+ import { noopLogger } from "@headless-lms/core/shared/logger";
3
4
  import { and, asc, eq, inArray, sql } from "drizzle-orm";
4
5
  import { ConflictError } from "@headless-lms/core/shared/errors";
5
- import { noopLogger } from "@headless-lms/core/shared/logger";
6
- import { parseRole } from "@headless-lms/core/organizations";
7
6
  //#region src/repositories/organizations.ts
8
7
  const INVITE_STATUSES = [
9
8
  "pending",
@@ -17,7 +16,7 @@ function toInvite(row) {
17
16
  id: row.id,
18
17
  orgId: row.orgId,
19
18
  email: row.email,
20
- role: row.role,
19
+ roleKey: row.roleKey,
21
20
  status: row.status,
22
21
  invitedBy: row.invitedBy,
23
22
  tokenHash: row.tokenHash,
@@ -67,6 +66,11 @@ var DrizzleOrganizationsRepository = class {
67
66
  const [row] = await this.db.select().from(organizations).where(eq(organizations.externalId, externalId)).limit(1);
68
67
  return row ?? null;
69
68
  }
69
+ async setMetadata(id, metadata) {
70
+ const [row] = await this.db.update(organizations).set({ metadata }).where(eq(organizations.id, id)).returning();
71
+ if (!row) throw new Error("failed to update organization");
72
+ return row;
73
+ }
70
74
  async findBySlug(slug) {
71
75
  const [row] = await this.db.select().from(organizations).where(eq(organizations.slug, slug)).limit(1);
72
76
  return row ?? null;
@@ -75,7 +79,7 @@ var DrizzleOrganizationsRepository = class {
75
79
  const [row] = await this.db.insert(invites).values({
76
80
  orgId,
77
81
  email: input.email,
78
- role: input.role,
82
+ roleKey: input.roleKey,
79
83
  status: "pending",
80
84
  invitedBy: input.invitedBy,
81
85
  tokenHash: input.tokenHash,
@@ -84,7 +88,7 @@ var DrizzleOrganizationsRepository = class {
84
88
  target: [invites.orgId, invites.email],
85
89
  targetWhere: sql`${invites.status} = 'pending'`,
86
90
  set: {
87
- role: input.role,
91
+ roleKey: input.roleKey,
88
92
  invitedBy: input.invitedBy,
89
93
  tokenHash: input.tokenHash,
90
94
  expiresAt: input.expiresAt
@@ -97,6 +101,13 @@ var DrizzleOrganizationsRepository = class {
97
101
  const [row] = await this.db.update(invites).set({ status: toStatus(status) }).where(and(eq(invites.orgId, orgId), eq(invites.id, id))).returning();
98
102
  return row ? toInvite(row) : null;
99
103
  }
104
+ async setInviteRole(orgId, id, roleKey, invitedBy) {
105
+ const [row] = await this.db.update(invites).set({
106
+ roleKey,
107
+ invitedBy
108
+ }).where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, "pending"))).returning();
109
+ return row ? toInvite(row) : null;
110
+ }
100
111
  async findInviteByTokenHash(tokenHash) {
101
112
  const [row] = await this.db.select().from(invites).where(eq(invites.tokenHash, tokenHash)).limit(1);
102
113
  return row ? toInvite(row) : null;
@@ -105,77 +116,46 @@ var DrizzleOrganizationsRepository = class {
105
116
  const [row] = await this.db.select().from(invites).where(and(eq(invites.orgId, orgId), sql`lower(${invites.email}) = lower(${email})`, eq(invites.status, "pending"))).limit(1);
106
117
  return row ? toInvite(row) : null;
107
118
  }
108
- async setInviteToken(orgId, id, tokenHash, expiresAt) {
119
+ async setInviteToken(orgId, id, tokenHash, expiresAt, invitedBy) {
109
120
  const [row] = await this.db.update(invites).set({
110
121
  tokenHash,
111
- expiresAt
122
+ expiresAt,
123
+ invitedBy
112
124
  }).where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, "pending"))).returning();
113
125
  return row ? toInvite(row) : null;
114
126
  }
115
127
  async findOrgUser(orgId, userId) {
116
128
  const [row] = await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId))).limit(1);
117
- return row ? {
118
- ...row,
119
- role: parseRole(row.role)
120
- } : null;
129
+ return row ?? null;
121
130
  }
122
131
  async findOrgUsersByUser(userId) {
123
- return (await this.db.select().from(orgUsers).where(eq(orgUsers.userId, userId)).orderBy(orgUsers.createdAt)).map((row) => ({
124
- ...row,
125
- role: parseRole(row.role)
126
- }));
132
+ return await this.db.select().from(orgUsers).where(eq(orgUsers.userId, userId)).orderBy(orgUsers.createdAt);
127
133
  }
128
134
  async findOrgUserById(orgId, id) {
129
135
  const [row] = await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).limit(1);
130
- return row ? {
131
- ...row,
132
- role: parseRole(row.role)
133
- } : null;
136
+ return row ?? null;
134
137
  }
135
138
  async findOrgUsersByIds(orgId, ids) {
136
139
  if (ids.length === 0) return [];
137
- return (await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)))).map((row) => ({
138
- ...row,
139
- role: parseRole(row.role)
140
- }));
141
- }
142
- async findOrgUserIdsByRole(orgId, roles) {
143
- if (roles.length === 0) return [];
144
- return (await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.status, "active"), inArray(orgUsers.role, roles))).orderBy(orgUsers.id)).map((row) => row.id);
140
+ return await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));
145
141
  }
146
142
  async createOrgUser(input) {
147
143
  try {
148
144
  const [row] = await this.db.insert(orgUsers).values({
149
145
  orgId: input.orgId,
150
146
  userId: input.userId,
151
- role: input.role,
152
147
  status: input.status ?? "active"
153
148
  }).returning();
154
149
  if (!row) throw new Error("failed to insert org user");
155
- return {
156
- ...row,
157
- role: parseRole(row.role)
158
- };
150
+ return row;
159
151
  } catch (err) {
160
152
  if (isUniqueViolation(err)) throw new ConflictError("That account already belongs to this organization");
161
153
  throw err;
162
154
  }
163
155
  }
164
156
  async updateOrgUser(orgId, id, patch) {
165
- const [row] = await this.db.update(orgUsers).set({
166
- ...patch.role !== void 0 && { role: patch.role },
167
- ...patch.status !== void 0 && { status: patch.status }
168
- }).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
169
- return row ? {
170
- ...row,
171
- role: parseRole(row.role)
172
- } : null;
173
- }
174
- async findStudentOrgUsers(userId) {
175
- return (await this.db.select().from(orgUsers).where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, "student"))).orderBy(orgUsers.createdAt)).map((row) => ({
176
- ...row,
177
- role: parseRole(row.role)
178
- }));
157
+ const [row] = await this.db.update(orgUsers).set({ ...patch.status !== void 0 && { status: patch.status } }).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
158
+ return row ?? null;
179
159
  }
180
160
  async deleteOrgUser(orgId, id) {
181
161
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, asc, eq, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n parseRole,\n type CreateOrgUserInput,\n type Invite,\n type NewInviteRow,\n type NewOrganizationRow,\n type Organization,\n type OrganizationsRepository,\n type OrgUser,\n type Role,\n type UpdateOrganizationInput,\n type UpdateOrgUserInput,\n} from \"@headless-lms/core/organizations\";\nimport { invites, organizations, orgUsers } from \"../schema/organizations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst INVITE_STATUSES = [\"pending\", \"accepted\", \"rejected\", \"canceled\"] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : \"pending\";\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n role: row.role,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert organization\");\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error(\"failed to delete organization\");\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async listIds(): Promise<string[]> {\n const rows = await this.db\n .select({ id: organizations.id })\n .from(organizations)\n .orderBy(asc(organizations.id));\n return rows.map((row) => row.id);\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n role: input.role,\n status: \"pending\",\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n role: input.role,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to upsert invite\");\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async setInviteToken(\n orgId: string,\n id: string,\n tokenHash: string,\n expiresAt: Date,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ tokenHash, expiresAt })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]> {\n if (roles.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n eq(orgUsers.status, \"active\"),\n inArray(orgUsers.role, roles),\n ),\n )\n .orderBy(orgUsers.id);\n return rows.map((row) => row.id);\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status: input.status ?? \"active\",\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert org user\");\n }\n return { ...row, role: parseRole(row.role) };\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"That account already belongs to this organization\");\n }\n throw err;\n }\n }\n\n async updateOrgUser(\n orgId: string,\n id: string,\n patch: UpdateOrgUserInput,\n ): Promise<OrgUser | null> {\n const [row] = await this.db\n .update(orgUsers)\n .set({\n ...(patch.role !== undefined && { role: patch.role }),\n ...(patch.status !== undefined && { status: patch.status }),\n })\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findStudentOrgUsers(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, \"student\")))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n try {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(\n new ConflictError(\n \"This person has written posts, comments or messages and cannot be deleted\",\n ),\n { cause: err },\n );\n }\n throw err;\n }\n }\n}\ntranslateDbErrors(DrizzleOrganizationsRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, asc, eq, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n CreateOrgUserInput,\n Invite,\n NewInviteRow,\n NewOrganizationRow,\n Organization,\n OrganizationMetadata,\n OrganizationsRepository,\n OrgUser,\n UpdateOrganizationInput,\n UpdateOrgUserInput,\n} from \"@headless-lms/core/organizations\";\nimport { invites, organizations, orgUsers } from \"../schema/organizations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst INVITE_STATUSES = [\"pending\", \"accepted\", \"rejected\", \"canceled\"] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : \"pending\";\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n roleKey: row.roleKey,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert organization\");\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error(\"failed to delete organization\");\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async listIds(): Promise<string[]> {\n const rows = await this.db\n .select({ id: organizations.id })\n .from(organizations)\n .orderBy(asc(organizations.id));\n return rows.map((row) => row.id);\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async setMetadata(id: string, metadata: OrganizationMetadata): Promise<Organization> {\n const [row] = await this.db\n .update(organizations)\n .set({ metadata })\n .where(eq(organizations.id, id))\n .returning();\n if (!row) {\n throw new Error(\"failed to update organization\");\n }\n return row;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n roleKey: input.roleKey,\n status: \"pending\",\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n roleKey: input.roleKey,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to upsert invite\");\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async setInviteRole(\n orgId: string,\n id: string,\n roleKey: string,\n invitedBy: string,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ roleKey, invitedBy })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async setInviteToken(\n orgId: string,\n id: string,\n tokenHash: string,\n expiresAt: Date,\n invitedBy: string,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ tokenHash, expiresAt, invitedBy })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ?? null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows;\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ?? null;\n }\n\n async findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));\n return rows;\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n status: input.status ?? \"active\",\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert org user\");\n }\n return row;\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"That account already belongs to this organization\");\n }\n throw err;\n }\n }\n\n async updateOrgUser(\n orgId: string,\n id: string,\n patch: UpdateOrgUserInput,\n ): Promise<OrgUser | null> {\n const [row] = await this.db\n .update(orgUsers)\n .set({\n ...(patch.status !== undefined && { status: patch.status }),\n })\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n try {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(\n new ConflictError(\n \"This person has written posts, comments or messages and cannot be deleted\",\n ),\n { cause: err },\n );\n }\n throw err;\n }\n }\n}\ntranslateDbErrors(DrizzleOrganizationsRepository);\n"],"mappings":"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"}
@@ -5,6 +5,10 @@ import { Logger, NewDomainEvent, OutboxAppender, OutboxMessage, OutboxStore } fr
5
5
  /** A message that fails this many dispatches is parked: fetchBatch stops
6
6
  * claiming it, leaving the row (with its last_error) for inspection. */
7
7
  declare const OUTBOX_MAX_ATTEMPTS = 10;
8
+ /** How long a fetched message stays claimed by the relay that fetched it. Another
9
+ * relay fetches it again only after this, so a relay that dies mid-batch delays
10
+ * its messages by at most this long. */
11
+ declare const OUTBOX_CLAIM_MS = 60000;
8
12
  declare class DrizzleOutboxAppender implements OutboxAppender {
9
13
  private readonly tx;
10
14
  private readonly logger;
@@ -20,5 +24,5 @@ declare class DrizzleOutboxStore implements OutboxStore {
20
24
  markFailed(id: string, error: string, nextAttemptAt: Date): Promise<void>;
21
25
  }
22
26
  //#endregion
23
- export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS };
27
+ export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS };
24
28
  //# sourceMappingURL=outbox.d.ts.map
@@ -1,11 +1,15 @@
1
1
  import { eventOutbox } from "../schema/outbox.js";
2
2
  import { translateDbErrors } from "./pg-errors.js";
3
- import { and, asc, eq, isNull, lt, lte, sql } from "drizzle-orm";
4
3
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { and, asc, eq, inArray, isNull, lt, lte, sql } from "drizzle-orm";
5
5
  //#region src/repositories/outbox.ts
6
6
  /** A message that fails this many dispatches is parked: fetchBatch stops
7
7
  * claiming it, leaving the row (with its last_error) for inspection. */
8
8
  const OUTBOX_MAX_ATTEMPTS = 10;
9
+ /** How long a fetched message stays claimed by the relay that fetched it. Another
10
+ * relay fetches it again only after this, so a relay that dies mid-batch delays
11
+ * its messages by at most this long. */
12
+ const OUTBOX_CLAIM_MS = 6e4;
9
13
  var DrizzleOutboxAppender = class {
10
14
  tx;
11
15
  logger;
@@ -30,7 +34,12 @@ var DrizzleOutboxStore = class {
30
34
  this.logger = logger;
31
35
  }
32
36
  async fetchBatch(limit) {
33
- return (await this.db.transaction((tx) => tx.select().from(eventOutbox).where(and(isNull(eventOutbox.processedAt), lte(eventOutbox.nextAttemptAt, /* @__PURE__ */ new Date()), lt(eventOutbox.attempts, 10))).orderBy(asc(eventOutbox.id)).limit(limit).for("update", { skipLocked: true }))).map((row) => ({
37
+ const now = /* @__PURE__ */ new Date();
38
+ return (await this.db.transaction(async (tx) => {
39
+ const due = await tx.select().from(eventOutbox).where(and(isNull(eventOutbox.processedAt), lte(eventOutbox.nextAttemptAt, now), lt(eventOutbox.attempts, 10))).orderBy(asc(eventOutbox.id)).limit(limit).for("update", { skipLocked: true });
40
+ if (due.length > 0) await tx.update(eventOutbox).set({ nextAttemptAt: new Date(now.getTime() + OUTBOX_CLAIM_MS) }).where(inArray(eventOutbox.id, due.map((row) => row.id)));
41
+ return due;
42
+ })).map((row) => ({
34
43
  id: row.id,
35
44
  attempts: row.attempts,
36
45
  payload: {
@@ -54,6 +63,6 @@ var DrizzleOutboxStore = class {
54
63
  translateDbErrors(DrizzleOutboxAppender);
55
64
  translateDbErrors(DrizzleOutboxStore);
56
65
  //#endregion
57
- export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS };
66
+ export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_CLAIM_MS, OUTBOX_MAX_ATTEMPTS };
58
67
 
59
68
  //# sourceMappingURL=outbox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"outbox.js","names":[],"sources":["../../src/repositories/outbox.ts"],"sourcesContent":["import { and, asc, eq, isNull, lt, lte, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DomainEvent,\n NewDomainEvent,\n OutboxAppender,\n OutboxMessage,\n OutboxStore,\n Logger,\n} from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { EventOutbox } from \"@headless-lms/core/schemas\";\nimport type { DbExecutor } from \"../client.js\";\nimport { eventOutbox } from \"../schema/outbox.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\n/** A message that fails this many dispatches is parked: fetchBatch stops\n * claiming it, leaving the row (with its last_error) for inspection. */\nexport const OUTBOX_MAX_ATTEMPTS = 10;\n\nexport class DrizzleOutboxAppender implements OutboxAppender {\n constructor(\n private readonly tx: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async append<E extends NewDomainEvent>(events: E[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n await this.tx.insert(eventOutbox).values(\n events.map((event) => ({\n type: event.type,\n orgId: event.orgId,\n payload: event as unknown as EventOutbox[\"payload\"],\n })),\n );\n }\n}\n\nexport class DrizzleOutboxStore implements OutboxStore {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async fetchBatch(limit: number): Promise<OutboxMessage[]> {\n const rows = await this.db.transaction((tx) =>\n tx\n .select()\n .from(eventOutbox)\n .where(\n and(\n isNull(eventOutbox.processedAt),\n lte(eventOutbox.nextAttemptAt, new Date()),\n lt(eventOutbox.attempts, OUTBOX_MAX_ATTEMPTS),\n ),\n )\n .orderBy(asc(eventOutbox.id))\n .limit(limit)\n .for(\"update\", { skipLocked: true }),\n );\n return rows.map((row) => ({\n id: row.id,\n attempts: row.attempts,\n payload: {\n ...row.payload,\n id: row.id,\n occurredAt: row.createdAt.toISOString(),\n } as unknown as DomainEvent,\n }));\n }\n\n async markProcessed(id: string): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({ processedAt: new Date() })\n .where(eq(eventOutbox.id, id));\n }\n\n async markFailed(id: string, error: string, nextAttemptAt: Date): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({\n attempts: sql`${eventOutbox.attempts} + 1`,\n lastError: error,\n nextAttemptAt,\n })\n .where(eq(eventOutbox.id, id));\n }\n}\ntranslateDbErrors(DrizzleOutboxAppender);\ntranslateDbErrors(DrizzleOutboxStore);\n"],"mappings":";;;;;;;AAkBA,MAAa,sBAAsB;AAEnC,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAiC,QAA4B;EACjE,IAAI,OAAO,WAAW,GACpB;EAEF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAChC,OAAO,KAAK,WAAW;GACrB,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,SAAS;EACX,EAAE,CACJ;CACF;AACF;AAEA,IAAa,qBAAb,MAAuD;CAElC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,WAAW,OAAyC;EAgBxD,QAAO,MAfY,KAAK,GAAG,aAAa,OACtC,GACG,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MACC,IACE,OAAO,YAAY,WAAW,GAC9B,IAAI,YAAY,+BAAe,IAAI,KAAK,CAAC,GACzC,GAAG,YAAY,UAAA,EAA6B,CAC9C,CACF,CAAC,CACA,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAC,CAC5B,MAAM,KAAK,CAAC,CACZ,IAAI,UAAU,EAAE,YAAY,KAAK,CAAC,CACvC,EAAA,CACY,KAAK,SAAS;GACxB,IAAI,IAAI;GACR,UAAU,IAAI;GACd,SAAS;IACP,GAAG,IAAI;IACP,IAAI,IAAI;IACR,YAAY,IAAI,UAAU,YAAY;GACxC;EACF,EAAE;CACJ;CAEA,MAAM,cAAc,IAA2B;EAC7C,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI,EAAE,6BAAa,IAAI,KAAK,EAAE,CAAC,CAAC,CAChC,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;CAEA,MAAM,WAAW,IAAY,OAAe,eAAoC;EAC9E,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GACH,UAAU,GAAG,GAAG,YAAY,SAAS;GACrC,WAAW;GACX;EACF,CAAC,CAAC,CACD,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;AACF;AACA,kBAAkB,qBAAqB;AACvC,kBAAkB,kBAAkB"}
1
+ {"version":3,"file":"outbox.js","names":[],"sources":["../../src/repositories/outbox.ts"],"sourcesContent":["import { and, asc, eq, inArray, isNull, lt, lte, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DomainEvent,\n NewDomainEvent,\n OutboxAppender,\n OutboxMessage,\n OutboxStore,\n Logger,\n} from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { EventOutbox } from \"@headless-lms/core/schemas\";\nimport type { DbExecutor } from \"../client.js\";\nimport { eventOutbox } from \"../schema/outbox.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\n/** A message that fails this many dispatches is parked: fetchBatch stops\n * claiming it, leaving the row (with its last_error) for inspection. */\nexport const OUTBOX_MAX_ATTEMPTS = 10;\n\n/** How long a fetched message stays claimed by the relay that fetched it. Another\n * relay fetches it again only after this, so a relay that dies mid-batch delays\n * its messages by at most this long. */\nexport const OUTBOX_CLAIM_MS = 60_000;\n\nexport class DrizzleOutboxAppender implements OutboxAppender {\n constructor(\n private readonly tx: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async append<E extends NewDomainEvent>(events: E[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n await this.tx.insert(eventOutbox).values(\n events.map((event) => ({\n type: event.type,\n orgId: event.orgId,\n payload: event as unknown as EventOutbox[\"payload\"],\n })),\n );\n }\n}\n\nexport class DrizzleOutboxStore implements OutboxStore {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async fetchBatch(limit: number): Promise<OutboxMessage[]> {\n const now = new Date();\n const rows = await this.db.transaction(async (tx) => {\n const due = await tx\n .select()\n .from(eventOutbox)\n .where(\n and(\n isNull(eventOutbox.processedAt),\n lte(eventOutbox.nextAttemptAt, now),\n lt(eventOutbox.attempts, OUTBOX_MAX_ATTEMPTS),\n ),\n )\n .orderBy(asc(eventOutbox.id))\n .limit(limit)\n .for(\"update\", { skipLocked: true });\n if (due.length > 0) {\n await tx\n .update(eventOutbox)\n .set({ nextAttemptAt: new Date(now.getTime() + OUTBOX_CLAIM_MS) })\n .where(\n inArray(\n eventOutbox.id,\n due.map((row) => row.id),\n ),\n );\n }\n return due;\n });\n return rows.map((row) => ({\n id: row.id,\n attempts: row.attempts,\n payload: {\n ...row.payload,\n id: row.id,\n occurredAt: row.createdAt.toISOString(),\n } as unknown as DomainEvent,\n }));\n }\n\n async markProcessed(id: string): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({ processedAt: new Date() })\n .where(eq(eventOutbox.id, id));\n }\n\n async markFailed(id: string, error: string, nextAttemptAt: Date): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({\n attempts: sql`${eventOutbox.attempts} + 1`,\n lastError: error,\n nextAttemptAt,\n })\n .where(eq(eventOutbox.id, id));\n }\n}\ntranslateDbErrors(DrizzleOutboxAppender);\ntranslateDbErrors(DrizzleOutboxStore);\n"],"mappings":";;;;;;;AAkBA,MAAa,sBAAsB;;;;AAKnC,MAAa,kBAAkB;AAE/B,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAiC,QAA4B;EACjE,IAAI,OAAO,WAAW,GACpB;EAEF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAChC,OAAO,KAAK,WAAW;GACrB,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,SAAS;EACX,EAAE,CACJ;CACF;AACF;AAEA,IAAa,qBAAb,MAAuD;CAElC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,WAAW,OAAyC;EACxD,MAAM,sBAAM,IAAI,KAAK;EA4BrB,QAAO,MA3BY,KAAK,GAAG,YAAY,OAAO,OAAO;GACnD,MAAM,MAAM,MAAM,GACf,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MACC,IACE,OAAO,YAAY,WAAW,GAC9B,IAAI,YAAY,eAAe,GAAG,GAClC,GAAG,YAAY,UAAA,EAA6B,CAC9C,CACF,CAAC,CACA,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAC,CAC5B,MAAM,KAAK,CAAC,CACZ,IAAI,UAAU,EAAE,YAAY,KAAK,CAAC;GACrC,IAAI,IAAI,SAAS,GACf,MAAM,GACH,OAAO,WAAW,CAAC,CACnB,IAAI,EAAE,eAAe,IAAI,KAAK,IAAI,QAAQ,IAAI,eAAe,EAAE,CAAC,CAAC,CACjE,MACC,QACE,YAAY,IACZ,IAAI,KAAK,QAAQ,IAAI,EAAE,CACzB,CACF;GAEJ,OAAO;EACT,CAAC,EAAA,CACW,KAAK,SAAS;GACxB,IAAI,IAAI;GACR,UAAU,IAAI;GACd,SAAS;IACP,GAAG,IAAI;IACP,IAAI,IAAI;IACR,YAAY,IAAI,UAAU,YAAY;GACxC;EACF,EAAE;CACJ;CAEA,MAAM,cAAc,IAA2B;EAC7C,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI,EAAE,6BAAa,IAAI,KAAK,EAAE,CAAC,CAAC,CAChC,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;CAEA,MAAM,WAAW,IAAY,OAAe,eAAoC;EAC9E,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GACH,UAAU,GAAG,GAAG,YAAY,SAAS;GACrC,WAAW;GACX;EACF,CAAC,CAAC,CACD,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;AACF;AACA,kBAAkB,qBAAqB;AACvC,kBAAkB,kBAAkB"}
@@ -2,8 +2,8 @@ import { comments, postBookmarks, postFlags, postReactions, posts } from "../sch
2
2
  import "../schema/index.js";
3
3
  import { translateDbErrors } from "./pg-errors.js";
4
4
  import { insertedRow } from "./inserted-row.js";
5
- import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
6
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
6
+ import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
7
7
  //#region src/repositories/posts.ts
8
8
  function toPost(row) {
9
9
  return {
@@ -1,7 +1,7 @@
1
1
  import { progressRecords } from "../schema/progress.js";
2
2
  import { translateDbErrors } from "./pg-errors.js";
3
- import { and, eq, inArray } from "drizzle-orm";
4
3
  import { noopLogger } from "@headless-lms/core/shared/logger";
4
+ import { and, eq, inArray } from "drizzle-orm";
5
5
  //#region src/repositories/progress.ts
6
6
  function toRecord(row) {
7
7
  return {