@headless-lms/adapter-db 0.2.0 → 0.4.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 (134) hide show
  1. package/dist/index.d.ts +12 -6
  2. package/dist/index.js +12 -6
  3. package/dist/repositories/admin.d.ts +23 -0
  4. package/dist/repositories/admin.js +393 -0
  5. package/dist/repositories/admin.js.map +1 -0
  6. package/dist/repositories/assets.d.ts +6 -3
  7. package/dist/repositories/assets.js +46 -9
  8. package/dist/repositories/assets.js.map +1 -1
  9. package/dist/repositories/automations.d.ts +5 -2
  10. package/dist/repositories/automations.js +29 -2
  11. package/dist/repositories/automations.js.map +1 -1
  12. package/dist/repositories/chats.d.ts +55 -0
  13. package/dist/repositories/chats.js +469 -0
  14. package/dist/repositories/chats.js.map +1 -0
  15. package/dist/repositories/content.d.ts +24 -1
  16. package/dist/repositories/content.js +316 -9
  17. package/dist/repositories/content.js.map +1 -1
  18. package/dist/repositories/devices.d.ts +17 -0
  19. package/dist/repositories/devices.js +75 -0
  20. package/dist/repositories/devices.js.map +1 -0
  21. package/dist/repositories/digest-runs.d.ts +14 -0
  22. package/dist/repositories/digest-runs.js +39 -0
  23. package/dist/repositories/digest-runs.js.map +1 -0
  24. package/dist/repositories/discussion.d.ts +16 -4
  25. package/dist/repositories/discussion.js +78 -18
  26. package/dist/repositories/discussion.js.map +1 -1
  27. package/dist/repositories/entitlements.d.ts +31 -9
  28. package/dist/repositories/entitlements.js +213 -17
  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 +32 -1
  35. package/dist/repositories/identity.js.map +1 -1
  36. package/dist/repositories/inserted-row.d.ts +5 -0
  37. package/dist/repositories/inserted-row.js +7 -0
  38. package/dist/repositories/inserted-row.js.map +1 -0
  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 +2 -2
  43. package/dist/repositories/members.js.map +1 -1
  44. package/dist/repositories/mentions.d.ts +17 -0
  45. package/dist/repositories/mentions.js +55 -0
  46. package/dist/repositories/mentions.js.map +1 -0
  47. package/dist/repositories/notification-settings.d.ts +19 -0
  48. package/dist/repositories/notification-settings.js +67 -0
  49. package/dist/repositories/notification-settings.js.map +1 -0
  50. package/dist/repositories/notifications.d.ts +30 -0
  51. package/dist/repositories/notifications.js +178 -0
  52. package/dist/repositories/notifications.js.map +1 -0
  53. package/dist/repositories/org-user-profile.d.ts +214 -1
  54. package/dist/repositories/org-user-profile.js +23 -2
  55. package/dist/repositories/org-user-profile.js.map +1 -1
  56. package/dist/repositories/organizations.d.ts +4 -2
  57. package/dist/repositories/organizations.js +23 -8
  58. package/dist/repositories/organizations.js.map +1 -1
  59. package/dist/repositories/pg-errors.js +3 -1
  60. package/dist/repositories/pg-errors.js.map +1 -1
  61. package/dist/repositories/posts.d.ts +35 -0
  62. package/dist/repositories/posts.js +252 -0
  63. package/dist/repositories/posts.js.map +1 -0
  64. package/dist/repositories/subscriptions.d.ts +18 -0
  65. package/dist/repositories/subscriptions.js +72 -0
  66. package/dist/repositories/subscriptions.js.map +1 -0
  67. package/dist/schema/assets.d.ts +19 -2
  68. package/dist/schema/assets.js +10 -4
  69. package/dist/schema/assets.js.map +1 -1
  70. package/dist/schema/automations.d.ts +545 -2
  71. package/dist/schema/automations.js +73 -3
  72. package/dist/schema/automations.js.map +1 -1
  73. package/dist/schema/better-auth.d.ts +1613 -238
  74. package/dist/schema/better-auth.js +107 -22
  75. package/dist/schema/better-auth.js.map +1 -1
  76. package/dist/schema/content.d.ts +642 -5
  77. package/dist/schema/content.js +119 -10
  78. package/dist/schema/content.js.map +1 -1
  79. package/dist/schema/discussion.d.ts +2182 -42
  80. package/dist/schema/discussion.js +461 -11
  81. package/dist/schema/discussion.js.map +1 -1
  82. package/dist/schema/entitlements.d.ts +432 -1
  83. package/dist/schema/entitlements.js +109 -4
  84. package/dist/schema/entitlements.js.map +1 -1
  85. package/dist/schema/identity.d.ts +517 -1
  86. package/dist/schema/identity.js +76 -2
  87. package/dist/schema/identity.js.map +1 -1
  88. package/dist/schema/index.d.ts +7 -7
  89. package/dist/schema/index.js +35 -8
  90. package/dist/schema/organizations.d.ts +4 -4
  91. package/dist/schema/organizations.js +2 -0
  92. package/dist/schema/organizations.js.map +1 -1
  93. package/drizzle/0003_community.sql +340 -0
  94. package/drizzle/0004_assets_sources.sql +1 -0
  95. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  96. package/drizzle/0006_community_review_fixes.sql +24 -0
  97. package/drizzle/0007_notifications.sql +37 -0
  98. package/drizzle/0008_content_access.sql +13 -0
  99. package/drizzle/0009_community_indexes.sql +56 -0
  100. package/drizzle/0010_mcp_access.sql +114 -0
  101. package/drizzle/0011_digest_runs.sql +8 -0
  102. package/drizzle/0012_subscriptions.sql +16 -0
  103. package/drizzle/0013_mentions.sql +10 -0
  104. package/drizzle/0014_notification_settings_devices.sql +36 -0
  105. package/drizzle/0015_automations_drop_source.sql +2 -0
  106. package/drizzle/0016_notifications_automations.sql +48 -0
  107. package/drizzle/meta/0003_snapshot.json +7061 -0
  108. package/drizzle/meta/0004_snapshot.json +7068 -0
  109. package/drizzle/meta/0005_snapshot.json +7180 -0
  110. package/drizzle/meta/0006_snapshot.json +7298 -0
  111. package/drizzle/meta/0007_snapshot.json +7620 -0
  112. package/drizzle/meta/0008_snapshot.json +7666 -0
  113. package/drizzle/meta/0009_snapshot.json +8185 -0
  114. package/drizzle/meta/0010_snapshot.json +8813 -0
  115. package/drizzle/meta/0011_snapshot.json +8868 -0
  116. package/drizzle/meta/0012_snapshot.json +8873 -0
  117. package/drizzle/meta/0013_snapshot.json +8943 -0
  118. package/drizzle/meta/0014_snapshot.json +9305 -0
  119. package/drizzle/meta/0015_snapshot.json +9270 -0
  120. package/drizzle/meta/0016_snapshot.json +9230 -0
  121. package/drizzle/meta/_journal.json +99 -1
  122. package/package.json +2 -2
  123. package/dist/repositories/course-analytics.d.ts +0 -15
  124. package/dist/repositories/course-analytics.js +0 -137
  125. package/dist/repositories/course-analytics.js.map +0 -1
  126. package/dist/repositories/dashboard.d.ts +0 -14
  127. package/dist/repositories/dashboard.js +0 -57
  128. package/dist/repositories/dashboard.js.map +0 -1
  129. package/dist/repositories/learn.d.ts +0 -19
  130. package/dist/repositories/learn.js +0 -56
  131. package/dist/repositories/learn.js.map +0 -1
  132. package/dist/repositories/students.d.ts +0 -16
  133. package/dist/repositories/students.js +0 -160
  134. package/dist/repositories/students.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { invites, orgUsers, organizations } from "../schema/organizations.js";
2
- import { isUniqueViolation, translateDbErrors } from "./pg-errors.js";
3
- import { and, eq, sql } from "drizzle-orm";
2
+ import { isUniqueViolation, pgErrorFields, translateDbErrors } from "./pg-errors.js";
3
+ import { and, asc, eq, inArray, sql } from "drizzle-orm";
4
4
  import { ConflictError } from "@headless-lms/core/shared/errors";
5
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
6
6
  import { parseRole } from "@headless-lms/core/organizations";
@@ -60,6 +60,9 @@ var DrizzleOrganizationsRepository = class {
60
60
  const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));
61
61
  return row ?? null;
62
62
  }
63
+ async listIds() {
64
+ return (await this.db.select({ id: organizations.id }).from(organizations).orderBy(asc(organizations.id))).map((row) => row.id);
65
+ }
63
66
  async findByExternalId(externalId) {
64
67
  const [row] = await this.db.select().from(organizations).where(eq(organizations.externalId, externalId)).limit(1);
65
68
  return row ?? null;
@@ -129,6 +132,17 @@ var DrizzleOrganizationsRepository = class {
129
132
  role: parseRole(row.role)
130
133
  } : null;
131
134
  }
135
+ async findOrgUsersByIds(orgId, ids) {
136
+ 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);
145
+ }
132
146
  async createOrgUser(input) {
133
147
  try {
134
148
  const [row] = await this.db.insert(orgUsers).values({
@@ -163,13 +177,14 @@ var DrizzleOrganizationsRepository = class {
163
177
  role: parseRole(row.role)
164
178
  }));
165
179
  }
166
- async cancelPendingInvite(orgId, email) {
167
- const [row] = await this.db.update(invites).set({ status: "canceled" }).where(and(eq(invites.orgId, orgId), sql`lower(${invites.email}) = lower(${email})`, eq(invites.status, "pending"))).returning();
168
- return row ? toInvite(row) : null;
169
- }
170
180
  async deleteOrgUser(orgId, id) {
171
- const [row] = await this.db.delete(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
172
- return row ?? null;
181
+ try {
182
+ const [row] = await this.db.delete(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
183
+ return row ?? null;
184
+ } catch (err) {
185
+ if (pgErrorFields(err)?.code === "23503") throw Object.assign(new ConflictError("This person has written posts, comments or messages and cannot be deleted"), { cause: err });
186
+ throw err;
187
+ }
173
188
  }
174
189
  };
175
190
  translateDbErrors(DrizzleOrganizationsRepository);
@@ -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, eq, 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 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, 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 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 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 cancelPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: \"canceled\" })\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n\n return row ?? null;\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 {\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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"}
@@ -95,7 +95,9 @@ function translateConstraintViolation(err) {
95
95
  const pg = pgErrorFields(err);
96
96
  if (pg?.code !== "23505") return;
97
97
  const key = pg.detail?.match(KEY_DETAIL);
98
- const conflict = new ConflictError(key ? `${key[1]} "${key[2]}" is already in use` : "This value is already in use");
98
+ const column = key?.[1];
99
+ const value = key?.[2];
100
+ const conflict = new ConflictError(column && value && !column.includes(",") ? `${column} "${value}" is already in use` : "This already exists");
99
101
  return Object.assign(conflict, { cause: err });
100
102
  }
101
103
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"pg-errors.js","names":[],"sources":["../../src/repositories/pg-errors.ts"],"sourcesContent":["import { ConflictError } from \"@headless-lms/core/shared/errors\";\n\n/**\n * Structured fields off a node-postgres DatabaseError. The driver (and\n * drizzle's DrizzleQueryError) sometimes wraps the original error behind\n * `cause`, so both levels are checked.\n */\nexport type PgErrorFields = {\n code: string;\n constraint?: string;\n table?: string;\n column?: string;\n detail?: string;\n};\n\nconst PG_CODE = /^[0-9A-Z]{5}$/;\n\nexport function pgErrorFields(err: unknown): PgErrorFields | undefined {\n for (const level of [err, (err as { cause?: unknown } | undefined)?.cause]) {\n const e = level as Partial<PgErrorFields> | undefined;\n if (typeof e?.code === \"string\" && PG_CODE.test(e.code)) {\n return {\n code: e.code,\n ...(typeof e.constraint === \"string\" && { constraint: e.constraint }),\n ...(typeof e.table === \"string\" && { table: e.table }),\n ...(typeof e.column === \"string\" && { column: e.column }),\n ...(typeof e.detail === \"string\" && { detail: e.detail }),\n };\n }\n }\n return undefined;\n}\n\n/** Postgres unique_violation (23505). */\nexport function isUniqueViolation(err: unknown): boolean {\n return pgErrorFields(err)?.code === \"23505\";\n}\n\n/**\n * A database failure sanitized for logging: the driver's message, the SQL\n * text, and the structured pg fields — never the bind params, which a\n * DrizzleQueryError embeds in its message and can carry PII.\n */\nexport class DbQueryError extends Error {\n readonly pg?: PgErrorFields;\n readonly query?: string;\n\n constructor(message: string, opts: { pg?: PgErrorFields; query?: string; cause?: Error }) {\n super(message, opts.cause && { cause: opts.cause });\n this.name = \"DbQueryError\";\n if (opts.pg) {\n this.pg = opts.pg;\n }\n if (opts.query) {\n this.query = opts.query;\n }\n }\n}\n\n/**\n * What the unit of work rethrows: constraint violations become domain\n * ConflictErrors, other query failures become sanitized DbQueryErrors, and\n * anything non-database passes through untouched.\n */\nexport function translateDbError(err: unknown): unknown {\n if (err instanceof ConflictError || err instanceof DbQueryError) {\n return err;\n }\n const conflict = translateConstraintViolation(err);\n if (conflict) {\n return conflict;\n }\n const e = err as { query?: unknown; params?: unknown; cause?: unknown; message?: unknown };\n if (typeof e?.query === \"string\" && Array.isArray(e.params)) {\n const cause = e.cause instanceof Error ? e.cause : undefined;\n const pg = pgErrorFields(err);\n return new DbQueryError(cause?.message ?? \"database query failed\", {\n ...(cause && { cause }),\n query: e.query,\n ...(pg && { pg }),\n });\n }\n return err;\n}\n\n/**\n * Route every method of a repository class through translateDbError, so no\n * drizzle or pg error ever leaves this package — regardless of whether the\n * call runs inside a unit of work. Applied once per class, right below its\n * declaration.\n */\nexport function translateDbErrors(cls: { prototype: object }): void {\n const proto = cls.prototype as Record<string, unknown>;\n for (const name of Object.getOwnPropertyNames(proto)) {\n if (name === \"constructor\") {\n continue;\n }\n const desc = Object.getOwnPropertyDescriptor(proto, name);\n if (!desc || typeof desc.value !== \"function\") {\n continue;\n }\n const original = desc.value as (...args: unknown[]) => unknown;\n proto[name] = function (this: unknown, ...args: unknown[]) {\n let result: unknown;\n try {\n result = original.apply(this, args);\n } catch (err) {\n throw translateDbError(err);\n }\n if (result instanceof Promise) {\n return result.catch((err: unknown) => {\n throw translateDbError(err);\n });\n }\n return result;\n };\n }\n}\n\n/** `Key (slug)=(orgie) already exists.` → ['slug', 'orgie'] */\nconst KEY_DETAIL = /^Key \\((.+?)\\)=\\((.+?)\\)/;\n\n/**\n * Only unique violations (23505) become ConflictErrors: a duplicate is the\n * one constraint failure the caller can act on, so it earns a 409 with a\n * client-facing message — the offending column and the caller's own value,\n * never pg's `detail`. Every other constraint failure (foreign keys\n * included) is a server-side integrity problem: it stays an internal error,\n * sanitized by translateDbError and logged, and the client sees a generic\n * 500. The raw error stays on `cause` for server-side logs.\n */\nexport function translateConstraintViolation(err: unknown): ConflictError | undefined {\n const pg = pgErrorFields(err);\n if (pg?.code !== \"23505\") {\n return undefined;\n }\n const key = pg.detail?.match(KEY_DETAIL);\n const conflict = new ConflictError(\n key ? `${key[1]} \"${key[2]}\" is already in use` : \"This value is already in use\",\n );\n return Object.assign(conflict, { cause: err });\n}\n"],"mappings":";;AAeA,MAAM,UAAU;AAEhB,SAAgB,cAAc,KAAyC;CACrE,KAAK,MAAM,SAAS,CAAC,KAAM,KAAyC,KAAK,GAAG;EAC1E,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,SAAS,YAAY,QAAQ,KAAK,EAAE,IAAI,GACpD,OAAO;GACL,MAAM,EAAE;GACR,GAAI,OAAO,EAAE,eAAe,YAAY,EAAE,YAAY,EAAE,WAAW;GACnE,GAAI,OAAO,EAAE,UAAU,YAAY,EAAE,OAAO,EAAE,MAAM;GACpD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;GACvD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;EACzD;CAEJ;AAEF;;AAGA,SAAgB,kBAAkB,KAAuB;CACvD,OAAO,cAAc,GAAG,CAAC,EAAE,SAAS;AACtC;;;;;;AAOA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,SAAiB,MAA6D;EACxF,MAAM,SAAS,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM,CAAC;EAClD,KAAK,OAAO;EACZ,IAAI,KAAK,IACP,KAAK,KAAK,KAAK;EAEjB,IAAI,KAAK,OACP,KAAK,QAAQ,KAAK;CAEtB;AACF;;;;;;AAOA,SAAgB,iBAAiB,KAAuB;CACtD,IAAI,eAAe,iBAAiB,eAAe,cACjD,OAAO;CAET,MAAM,WAAW,6BAA6B,GAAG;CACjD,IAAI,UACF,OAAO;CAET,MAAM,IAAI;CACV,IAAI,OAAO,GAAG,UAAU,YAAY,MAAM,QAAQ,EAAE,MAAM,GAAG;EAC3D,MAAM,QAAQ,EAAE,iBAAiB,QAAQ,EAAE,QAAQ,KAAA;EACnD,MAAM,KAAK,cAAc,GAAG;EAC5B,OAAO,IAAI,aAAa,OAAO,WAAW,yBAAyB;GACjE,GAAI,SAAS,EAAE,MAAM;GACrB,OAAO,EAAE;GACT,GAAI,MAAM,EAAE,GAAG;EACjB,CAAC;CACH;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAkB,KAAkC;CAClE,MAAM,QAAQ,IAAI;CAClB,KAAK,MAAM,QAAQ,OAAO,oBAAoB,KAAK,GAAG;EACpD,IAAI,SAAS,eACX;EAEF,MAAM,OAAO,OAAO,yBAAyB,OAAO,IAAI;EACxD,IAAI,CAAC,QAAQ,OAAO,KAAK,UAAU,YACjC;EAEF,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,SAAyB,GAAG,MAAiB;GACzD,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,MAAM,MAAM,IAAI;GACpC,SAAS,KAAK;IACZ,MAAM,iBAAiB,GAAG;GAC5B;GACA,IAAI,kBAAkB,SACpB,OAAO,OAAO,OAAO,QAAiB;IACpC,MAAM,iBAAiB,GAAG;GAC5B,CAAC;GAEH,OAAO;EACT;CACF;AACF;;AAGA,MAAM,aAAa;;;;;;;;;;AAWnB,SAAgB,6BAA6B,KAAyC;CACpF,MAAM,KAAK,cAAc,GAAG;CAC5B,IAAI,IAAI,SAAS,SACf;CAEF,MAAM,MAAM,GAAG,QAAQ,MAAM,UAAU;CACvC,MAAM,WAAW,IAAI,cACnB,MAAM,GAAG,IAAI,GAAG,IAAI,IAAI,GAAG,uBAAuB,8BACpD;CACA,OAAO,OAAO,OAAO,UAAU,EAAE,OAAO,IAAI,CAAC;AAC/C"}
1
+ {"version":3,"file":"pg-errors.js","names":[],"sources":["../../src/repositories/pg-errors.ts"],"sourcesContent":["import { ConflictError } from \"@headless-lms/core/shared/errors\";\n\n/**\n * Structured fields off a node-postgres DatabaseError. The driver (and\n * drizzle's DrizzleQueryError) sometimes wraps the original error behind\n * `cause`, so both levels are checked.\n */\nexport type PgErrorFields = {\n code: string;\n constraint?: string;\n table?: string;\n column?: string;\n detail?: string;\n};\n\nconst PG_CODE = /^[0-9A-Z]{5}$/;\n\nexport function pgErrorFields(err: unknown): PgErrorFields | undefined {\n for (const level of [err, (err as { cause?: unknown } | undefined)?.cause]) {\n const e = level as Partial<PgErrorFields> | undefined;\n if (typeof e?.code === \"string\" && PG_CODE.test(e.code)) {\n return {\n code: e.code,\n ...(typeof e.constraint === \"string\" && { constraint: e.constraint }),\n ...(typeof e.table === \"string\" && { table: e.table }),\n ...(typeof e.column === \"string\" && { column: e.column }),\n ...(typeof e.detail === \"string\" && { detail: e.detail }),\n };\n }\n }\n return undefined;\n}\n\n/** Postgres unique_violation (23505). */\nexport function isUniqueViolation(err: unknown): boolean {\n return pgErrorFields(err)?.code === \"23505\";\n}\n\n/**\n * A database failure sanitized for logging: the driver's message, the SQL\n * text, and the structured pg fields — never the bind params, which a\n * DrizzleQueryError embeds in its message and can carry PII.\n */\nexport class DbQueryError extends Error {\n readonly pg?: PgErrorFields;\n readonly query?: string;\n\n constructor(message: string, opts: { pg?: PgErrorFields; query?: string; cause?: Error }) {\n super(message, opts.cause && { cause: opts.cause });\n this.name = \"DbQueryError\";\n if (opts.pg) {\n this.pg = opts.pg;\n }\n if (opts.query) {\n this.query = opts.query;\n }\n }\n}\n\n/**\n * What the unit of work rethrows: constraint violations become domain\n * ConflictErrors, other query failures become sanitized DbQueryErrors, and\n * anything non-database passes through untouched.\n */\nexport function translateDbError(err: unknown): unknown {\n if (err instanceof ConflictError || err instanceof DbQueryError) {\n return err;\n }\n const conflict = translateConstraintViolation(err);\n if (conflict) {\n return conflict;\n }\n const e = err as { query?: unknown; params?: unknown; cause?: unknown; message?: unknown };\n if (typeof e?.query === \"string\" && Array.isArray(e.params)) {\n const cause = e.cause instanceof Error ? e.cause : undefined;\n const pg = pgErrorFields(err);\n return new DbQueryError(cause?.message ?? \"database query failed\", {\n ...(cause && { cause }),\n query: e.query,\n ...(pg && { pg }),\n });\n }\n return err;\n}\n\n/**\n * Route every method of a repository class through translateDbError, so no\n * drizzle or pg error ever leaves this package — regardless of whether the\n * call runs inside a unit of work. Applied once per class, right below its\n * declaration.\n */\nexport function translateDbErrors(cls: { prototype: object }): void {\n const proto = cls.prototype as Record<string, unknown>;\n for (const name of Object.getOwnPropertyNames(proto)) {\n if (name === \"constructor\") {\n continue;\n }\n const desc = Object.getOwnPropertyDescriptor(proto, name);\n if (!desc || typeof desc.value !== \"function\") {\n continue;\n }\n const original = desc.value as (...args: unknown[]) => unknown;\n proto[name] = function (this: unknown, ...args: unknown[]) {\n let result: unknown;\n try {\n result = original.apply(this, args);\n } catch (err) {\n throw translateDbError(err);\n }\n if (result instanceof Promise) {\n return result.catch((err: unknown) => {\n throw translateDbError(err);\n });\n }\n return result;\n };\n }\n}\n\n/** `Key (slug)=(orgie) already exists.` → ['slug', 'orgie'] */\nconst KEY_DETAIL = /^Key \\((.+?)\\)=\\((.+?)\\)/;\n\n/**\n * Only unique violations (23505) become ConflictErrors: a duplicate is the\n * one constraint failure the caller can act on, so it earns a 409 with a\n * client-facing message — the offending column and the caller's own value,\n * never pg's `detail`. Every other constraint failure (foreign keys\n * included) is a server-side integrity problem: it stays an internal error,\n * sanitized by translateDbError and logged, and the client sees a generic\n * 500. The raw error stays on `cause` for server-side logs.\n */\nexport function translateConstraintViolation(err: unknown): ConflictError | undefined {\n const pg = pgErrorFields(err);\n if (pg?.code !== \"23505\") {\n return undefined;\n }\n const key = pg.detail?.match(KEY_DETAIL);\n const column = key?.[1];\n const value = key?.[2];\n const conflict = new ConflictError(\n column && value && !column.includes(\",\")\n ? `${column} \"${value}\" is already in use`\n : \"This already exists\",\n );\n return Object.assign(conflict, { cause: err });\n}\n"],"mappings":";;AAeA,MAAM,UAAU;AAEhB,SAAgB,cAAc,KAAyC;CACrE,KAAK,MAAM,SAAS,CAAC,KAAM,KAAyC,KAAK,GAAG;EAC1E,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,SAAS,YAAY,QAAQ,KAAK,EAAE,IAAI,GACpD,OAAO;GACL,MAAM,EAAE;GACR,GAAI,OAAO,EAAE,eAAe,YAAY,EAAE,YAAY,EAAE,WAAW;GACnE,GAAI,OAAO,EAAE,UAAU,YAAY,EAAE,OAAO,EAAE,MAAM;GACpD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;GACvD,GAAI,OAAO,EAAE,WAAW,YAAY,EAAE,QAAQ,EAAE,OAAO;EACzD;CAEJ;AAEF;;AAGA,SAAgB,kBAAkB,KAAuB;CACvD,OAAO,cAAc,GAAG,CAAC,EAAE,SAAS;AACtC;;;;;;AAOA,IAAa,eAAb,cAAkC,MAAM;CACtC;CACA;CAEA,YAAY,SAAiB,MAA6D;EACxF,MAAM,SAAS,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM,CAAC;EAClD,KAAK,OAAO;EACZ,IAAI,KAAK,IACP,KAAK,KAAK,KAAK;EAEjB,IAAI,KAAK,OACP,KAAK,QAAQ,KAAK;CAEtB;AACF;;;;;;AAOA,SAAgB,iBAAiB,KAAuB;CACtD,IAAI,eAAe,iBAAiB,eAAe,cACjD,OAAO;CAET,MAAM,WAAW,6BAA6B,GAAG;CACjD,IAAI,UACF,OAAO;CAET,MAAM,IAAI;CACV,IAAI,OAAO,GAAG,UAAU,YAAY,MAAM,QAAQ,EAAE,MAAM,GAAG;EAC3D,MAAM,QAAQ,EAAE,iBAAiB,QAAQ,EAAE,QAAQ,KAAA;EACnD,MAAM,KAAK,cAAc,GAAG;EAC5B,OAAO,IAAI,aAAa,OAAO,WAAW,yBAAyB;GACjE,GAAI,SAAS,EAAE,MAAM;GACrB,OAAO,EAAE;GACT,GAAI,MAAM,EAAE,GAAG;EACjB,CAAC;CACH;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAAkB,KAAkC;CAClE,MAAM,QAAQ,IAAI;CAClB,KAAK,MAAM,QAAQ,OAAO,oBAAoB,KAAK,GAAG;EACpD,IAAI,SAAS,eACX;EAEF,MAAM,OAAO,OAAO,yBAAyB,OAAO,IAAI;EACxD,IAAI,CAAC,QAAQ,OAAO,KAAK,UAAU,YACjC;EAEF,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,SAAyB,GAAG,MAAiB;GACzD,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,MAAM,MAAM,IAAI;GACpC,SAAS,KAAK;IACZ,MAAM,iBAAiB,GAAG;GAC5B;GACA,IAAI,kBAAkB,SACpB,OAAO,OAAO,OAAO,QAAiB;IACpC,MAAM,iBAAiB,GAAG;GAC5B,CAAC;GAEH,OAAO;EACT;CACF;AACF;;AAGA,MAAM,aAAa;;;;;;;;;;AAWnB,SAAgB,6BAA6B,KAAyC;CACpF,MAAM,KAAK,cAAc,GAAG;CAC5B,IAAI,IAAI,SAAS,SACf;CAEF,MAAM,MAAM,GAAG,QAAQ,MAAM,UAAU;CACvC,MAAM,SAAS,MAAM;CACrB,MAAM,QAAQ,MAAM;CACpB,MAAM,WAAW,IAAI,cACnB,UAAU,SAAS,CAAC,OAAO,SAAS,GAAG,IACnC,GAAG,OAAO,IAAI,MAAM,uBACpB,qBACN;CACA,OAAO,OAAO,OAAO,UAAU,EAAE,OAAO,IAAI,CAAC;AAC/C"}
@@ -0,0 +1,35 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, Page, Post, PostFlag, PostFlagResolution, ReactionEmoji } from "@headless-lms/core/types";
3
+ import { BookmarkQuery, FeedQuery, PageQuery, PostGuard, PostPatch, PostReactions, PostRepository, PublishedPost } from "@headless-lms/core/discussion";
4
+ //#region src/repositories/posts.d.ts
5
+ declare class DrizzlePostRepository implements PostRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ insert(orgId: string, post: Post): Promise<Post>;
10
+ update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;
11
+ updateIf(orgId: string, id: string, guard: PostGuard, patch: PostPatch): Promise<Post | null>;
12
+ find(orgId: string, id: string): Promise<Post | null>;
13
+ findMany(orgId: string, ids: string[]): Promise<Post[]>;
14
+ publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
15
+ listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;
16
+ listByAuthor(orgId: string, orgUserId: string, spaceIds: string[], query: PageQuery): Promise<Page<Post>>;
17
+ listBookmarked(orgId: string, orgUserId: string, spaceIds: string[], query: BookmarkQuery): Promise<Page<Post>>;
18
+ listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;
19
+ reactionsOf(orgId: string, postIds: string[], viewerId: string): Promise<Record<string, PostReactions>>;
20
+ setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
21
+ bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
22
+ unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
23
+ bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
24
+ insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null>;
25
+ findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null>;
26
+ listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]>;
27
+ resolveFlags(orgId: string, postId: string, resolved: {
28
+ resolvedAt: Date;
29
+ resolvedBy: string;
30
+ resolution: PostFlagResolution;
31
+ }): Promise<void>;
32
+ }
33
+ //#endregion
34
+ export { DrizzlePostRepository };
35
+ //# sourceMappingURL=posts.d.ts.map
@@ -0,0 +1,252 @@
1
+ import { comments, postBookmarks, postFlags, postReactions, posts } from "../schema/discussion.js";
2
+ import "../schema/index.js";
3
+ import { translateDbErrors } from "./pg-errors.js";
4
+ import { insertedRow } from "./inserted-row.js";
5
+ import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
6
+ import { noopLogger } from "@headless-lms/core/shared/logger";
7
+ //#region src/repositories/posts.ts
8
+ function toPost(row) {
9
+ return {
10
+ orgId: row.orgId,
11
+ id: row.id,
12
+ communityId: row.communityId,
13
+ spaceId: row.spaceId,
14
+ orgUserId: row.orgUserId,
15
+ topicId: row.topicId,
16
+ title: row.title,
17
+ body: row.body,
18
+ extra: row.extra,
19
+ coverAssetId: row.coverAssetId,
20
+ status: row.status,
21
+ scheduledAt: row.scheduledAt,
22
+ publishedAt: row.publishedAt,
23
+ removedBy: row.removedBy,
24
+ pinned: row.pinned,
25
+ commentsClosed: row.commentsClosed,
26
+ reactionsEnabled: row.reactionsEnabled,
27
+ createdAt: row.createdAt,
28
+ updatedAt: row.updatedAt
29
+ };
30
+ }
31
+ function toFlag(row) {
32
+ return {
33
+ orgId: row.orgId,
34
+ id: row.id,
35
+ postId: row.postId,
36
+ orgUserId: row.orgUserId,
37
+ reason: row.reason,
38
+ resolvedAt: row.resolvedAt,
39
+ resolution: row.resolution,
40
+ resolvedBy: row.resolvedBy,
41
+ createdAt: row.createdAt,
42
+ updatedAt: row.updatedAt
43
+ };
44
+ }
45
+ var DrizzlePostRepository = class {
46
+ db;
47
+ logger;
48
+ constructor(db, logger = noopLogger) {
49
+ this.db = db;
50
+ this.logger = logger;
51
+ }
52
+ async insert(orgId, post) {
53
+ const [row] = await this.db.insert(posts).values({
54
+ orgId,
55
+ id: post.id,
56
+ communityId: post.communityId,
57
+ spaceId: post.spaceId,
58
+ orgUserId: post.orgUserId,
59
+ topicId: post.topicId,
60
+ title: post.title,
61
+ body: post.body,
62
+ extra: post.extra,
63
+ coverAssetId: post.coverAssetId,
64
+ status: post.status,
65
+ scheduledAt: post.scheduledAt,
66
+ publishedAt: post.publishedAt,
67
+ removedBy: post.removedBy,
68
+ pinned: post.pinned,
69
+ commentsClosed: post.commentsClosed,
70
+ reactionsEnabled: post.reactionsEnabled,
71
+ createdAt: post.createdAt,
72
+ updatedAt: post.updatedAt
73
+ }).returning();
74
+ this.logger.debug("post inserted", {
75
+ orgId,
76
+ postId: post.id
77
+ });
78
+ return toPost(row);
79
+ }
80
+ async update(orgId, id, patch) {
81
+ const [row] = await this.db.update(posts).set({
82
+ ...patch,
83
+ updatedAt: /* @__PURE__ */ new Date()
84
+ }).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).returning();
85
+ return row ? toPost(row) : null;
86
+ }
87
+ async updateIf(orgId, id, guard, patch) {
88
+ const [row] = await this.db.update(posts).set({
89
+ ...patch,
90
+ updatedAt: /* @__PURE__ */ new Date()
91
+ }).where(and(eq(posts.orgId, orgId), eq(posts.id, id), eq(posts.status, guard.status), guard.scheduledAt === void 0 ? void 0 : eq(posts.scheduledAt, guard.scheduledAt), guard.dueBy === void 0 ? void 0 : lte(posts.scheduledAt, guard.dueBy))).returning();
92
+ return row ? toPost(row) : null;
93
+ }
94
+ async find(orgId, id) {
95
+ const [row] = await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).limit(1);
96
+ return row ? toPost(row) : null;
97
+ }
98
+ async findMany(orgId, ids) {
99
+ if (ids.length === 0) return [];
100
+ return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), inArray(posts.id, ids)))).map(toPost);
101
+ }
102
+ async publishedBetween(orgId, communityId, from, to) {
103
+ return (await this.db.select({
104
+ postId: posts.id,
105
+ spaceId: posts.spaceId,
106
+ title: posts.title,
107
+ orgUserId: posts.orgUserId,
108
+ commentCount: sql`(select count(*)::int from ${comments} where ${comments.orgId} = ${posts.orgId} and ${comments.postId} = ${posts.id} and ${comments.status} = 'published')`
109
+ }).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.communityId, communityId), eq(posts.status, "published"), gt(posts.publishedAt, from), lte(posts.publishedAt, to))).orderBy(asc(posts.publishedAt), asc(posts.id))).map((row) => ({
110
+ ...row,
111
+ commentCount: Number(row.commentCount)
112
+ }));
113
+ }
114
+ async listPublished(orgId, spaceIds, query) {
115
+ if (spaceIds.length === 0) return {
116
+ rows: [],
117
+ total: 0,
118
+ page: query.page,
119
+ pageSize: query.pageSize
120
+ };
121
+ const where = and(eq(posts.orgId, orgId), inArray(posts.spaceId, spaceIds), eq(posts.status, "published"), query.topicId ? eq(posts.topicId, query.topicId) : void 0);
122
+ const rows = await this.db.select().from(posts).where(where).orderBy(desc(posts.pinned), desc(posts.publishedAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
123
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(posts).where(where);
124
+ return {
125
+ rows: rows.map(toPost),
126
+ total,
127
+ page: query.page,
128
+ pageSize: query.pageSize
129
+ };
130
+ }
131
+ async listByAuthor(orgId, orgUserId, spaceIds, query) {
132
+ if (spaceIds.length === 0) return {
133
+ rows: [],
134
+ total: 0,
135
+ page: query.page,
136
+ pageSize: query.pageSize
137
+ };
138
+ const where = and(eq(posts.orgId, orgId), eq(posts.orgUserId, orgUserId), inArray(posts.spaceId, spaceIds));
139
+ const rows = await this.db.select().from(posts).where(where).orderBy(desc(posts.createdAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
140
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(posts).where(where);
141
+ return {
142
+ rows: rows.map(toPost),
143
+ total,
144
+ page: query.page,
145
+ pageSize: query.pageSize
146
+ };
147
+ }
148
+ async listBookmarked(orgId, orgUserId, spaceIds, query) {
149
+ if (spaceIds.length === 0) return {
150
+ rows: [],
151
+ total: 0,
152
+ page: query.page,
153
+ pageSize: query.pageSize
154
+ };
155
+ const where = and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.orgUserId, orgUserId), inArray(posts.spaceId, spaceIds), or(eq(posts.orgUserId, orgUserId), inArray(posts.status, query.visibleStatuses)));
156
+ const rows = await this.db.select({ post: posts }).from(postBookmarks).innerJoin(posts, and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId))).where(where).orderBy(desc(postBookmarks.createdAt), desc(posts.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
157
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(postBookmarks).innerJoin(posts, and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId))).where(where);
158
+ return {
159
+ rows: rows.map((row) => toPost(row.post)),
160
+ total,
161
+ page: query.page,
162
+ pageSize: query.pageSize
163
+ };
164
+ }
165
+ async listPending(orgId, spaceId, limit) {
166
+ return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), eq(posts.status, "pending"))).orderBy(asc(posts.createdAt), asc(posts.id)).limit(limit)).map(toPost);
167
+ }
168
+ async reactionsOf(orgId, postIds, viewerId) {
169
+ if (postIds.length === 0) return {};
170
+ const rows = await this.db.select({
171
+ postId: postReactions.postId,
172
+ emoji: postReactions.emoji,
173
+ count: sql`cast(count(*) as int)`,
174
+ mine: sql`bool_or(${postReactions.orgUserId} = ${viewerId})`
175
+ }).from(postReactions).where(and(eq(postReactions.orgId, orgId), inArray(postReactions.postId, postIds))).groupBy(postReactions.postId, postReactions.emoji);
176
+ const byPost = {};
177
+ for (const row of rows) {
178
+ const entry = byPost[row.postId] ??= { reactions: {} };
179
+ entry.reactions[row.emoji] = row.count;
180
+ if (row.mine) entry.viewerReaction = row.emoji;
181
+ }
182
+ return byPost;
183
+ }
184
+ async setReaction(orgId, postId, orgUserId, emoji) {
185
+ if (emoji === null) {
186
+ await this.db.delete(postReactions).where(and(eq(postReactions.orgId, orgId), eq(postReactions.postId, postId), eq(postReactions.orgUserId, orgUserId)));
187
+ return false;
188
+ }
189
+ const [row] = await this.db.insert(postReactions).values({
190
+ orgId,
191
+ postId,
192
+ orgUserId,
193
+ emoji
194
+ }).onConflictDoUpdate({
195
+ target: [
196
+ postReactions.orgId,
197
+ postReactions.postId,
198
+ postReactions.orgUserId
199
+ ],
200
+ set: { emoji }
201
+ }).returning({ inserted: insertedRow });
202
+ return row?.inserted === true;
203
+ }
204
+ async bookmark(orgId, postId, orgUserId) {
205
+ await this.db.insert(postBookmarks).values({
206
+ orgId,
207
+ postId,
208
+ orgUserId
209
+ }).onConflictDoNothing();
210
+ }
211
+ async unbookmark(orgId, postId, orgUserId) {
212
+ await this.db.delete(postBookmarks).where(and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.postId, postId), eq(postBookmarks.orgUserId, orgUserId)));
213
+ }
214
+ async bookmarkedAmong(orgId, postIds, orgUserId) {
215
+ if (postIds.length === 0) return [];
216
+ return (await this.db.select({ postId: postBookmarks.postId }).from(postBookmarks).where(and(eq(postBookmarks.orgId, orgId), eq(postBookmarks.orgUserId, orgUserId), inArray(postBookmarks.postId, postIds)))).map((row) => row.postId);
217
+ }
218
+ async insertFlag(orgId, flag) {
219
+ const [row] = await this.db.insert(postFlags).values({
220
+ orgId,
221
+ id: flag.id,
222
+ postId: flag.postId,
223
+ orgUserId: flag.orgUserId,
224
+ reason: flag.reason,
225
+ resolvedAt: flag.resolvedAt,
226
+ resolution: flag.resolution,
227
+ resolvedBy: flag.resolvedBy,
228
+ createdAt: flag.createdAt,
229
+ updatedAt: flag.updatedAt
230
+ }).onConflictDoNothing().returning();
231
+ return row ? toFlag(row) : null;
232
+ }
233
+ async findFlag(orgId, postId, orgUserId) {
234
+ const [row] = await this.db.select().from(postFlags).where(and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), eq(postFlags.orgUserId, orgUserId))).limit(1);
235
+ return row ? toFlag(row) : null;
236
+ }
237
+ async listOpenFlags(orgId, spaceId, limit) {
238
+ return (await this.db.select({ flag: postFlags }).from(postFlags).innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId))).where(and(eq(postFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(postFlags.resolvedAt))).orderBy(asc(postFlags.createdAt), asc(postFlags.id)).limit(limit)).map((row) => toFlag(row.flag));
239
+ }
240
+ async resolveFlags(orgId, postId, resolved) {
241
+ await this.db.update(postFlags).set({
242
+ resolvedAt: resolved.resolvedAt,
243
+ resolvedBy: resolved.resolvedBy,
244
+ resolution: resolved.resolution
245
+ }).where(and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), isNull(postFlags.resolvedAt)));
246
+ }
247
+ };
248
+ translateDbErrors(DrizzlePostRepository);
249
+ //#endregion
250
+ export { DrizzlePostRepository };
251
+
252
+ //# sourceMappingURL=posts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posts.js","names":[],"sources":["../../src/repositories/posts.ts"],"sourcesContent":["import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n BookmarkQuery,\n FeedQuery,\n PageQuery,\n PostGuard,\n PostReactions,\n PostRepository,\n PostPatch,\n PublishedPost,\n} from \"@headless-lms/core/discussion\";\nimport type {\n Logger,\n Page,\n Post,\n PostFlag,\n PostFlagResolution,\n ReactionEmoji,\n} from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { comments, postFlags, postBookmarks, postReactions, posts } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { insertedRow } from \"./inserted-row.js\";\n\ntype PostRow = typeof posts.$inferSelect;\ntype FlagRow = typeof postFlags.$inferSelect;\n\nfunction toPost(row: PostRow): Post {\n return {\n orgId: row.orgId,\n id: row.id,\n communityId: row.communityId,\n spaceId: row.spaceId,\n orgUserId: row.orgUserId,\n topicId: row.topicId,\n title: row.title,\n body: row.body,\n extra: row.extra,\n coverAssetId: row.coverAssetId,\n status: row.status,\n scheduledAt: row.scheduledAt,\n publishedAt: row.publishedAt,\n removedBy: row.removedBy,\n pinned: row.pinned,\n commentsClosed: row.commentsClosed,\n reactionsEnabled: row.reactionsEnabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction toFlag(row: FlagRow): PostFlag {\n return {\n orgId: row.orgId,\n id: row.id,\n postId: row.postId,\n orgUserId: row.orgUserId,\n reason: row.reason,\n resolvedAt: row.resolvedAt,\n resolution: row.resolution,\n resolvedBy: row.resolvedBy,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzlePostRepository implements PostRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, post: Post): Promise<Post> {\n const [row] = await this.db\n .insert(posts)\n .values({\n orgId,\n id: post.id,\n communityId: post.communityId,\n spaceId: post.spaceId,\n orgUserId: post.orgUserId,\n topicId: post.topicId,\n title: post.title,\n body: post.body,\n extra: post.extra,\n coverAssetId: post.coverAssetId,\n status: post.status,\n scheduledAt: post.scheduledAt,\n publishedAt: post.publishedAt,\n removedBy: post.removedBy,\n pinned: post.pinned,\n commentsClosed: post.commentsClosed,\n reactionsEnabled: post.reactionsEnabled,\n createdAt: post.createdAt,\n updatedAt: post.updatedAt,\n })\n .returning();\n this.logger.debug(\"post inserted\", { orgId, postId: post.id });\n return toPost(row!);\n }\n\n async update(orgId: string, id: string, patch: PostPatch): Promise<Post | null> {\n const [row] = await this.db\n .update(posts)\n .set({ ...patch, updatedAt: new Date() })\n .where(and(eq(posts.orgId, orgId), eq(posts.id, id)))\n .returning();\n return row ? toPost(row) : null;\n }\n\n async updateIf(\n orgId: string,\n id: string,\n guard: PostGuard,\n patch: PostPatch,\n ): Promise<Post | null> {\n const [row] = await this.db\n .update(posts)\n .set({ ...patch, updatedAt: new Date() })\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.id, id),\n eq(posts.status, guard.status),\n guard.scheduledAt === undefined ? undefined : eq(posts.scheduledAt, guard.scheduledAt),\n guard.dueBy === undefined ? undefined : lte(posts.scheduledAt, guard.dueBy),\n ),\n )\n .returning();\n return row ? toPost(row) : null;\n }\n\n async find(orgId: string, id: string): Promise<Post | null> {\n const [row] = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), eq(posts.id, id)))\n .limit(1);\n return row ? toPost(row) : null;\n }\n\n async findMany(orgId: string, ids: string[]): Promise<Post[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), inArray(posts.id, ids)));\n return rows.map(toPost);\n }\n\n async publishedBetween(\n orgId: string,\n communityId: string,\n from: Date,\n to: Date,\n ): Promise<PublishedPost[]> {\n const rows = await this.db\n .select({\n postId: posts.id,\n spaceId: posts.spaceId,\n title: posts.title,\n orgUserId: posts.orgUserId,\n commentCount: sql<number>`(select count(*)::int from ${comments} where ${comments.orgId} = ${posts.orgId} and ${comments.postId} = ${posts.id} and ${comments.status} = 'published')`,\n })\n .from(posts)\n .where(\n and(\n eq(posts.orgId, orgId),\n eq(posts.communityId, communityId),\n eq(posts.status, \"published\"),\n gt(posts.publishedAt, from),\n lte(posts.publishedAt, to),\n ),\n )\n .orderBy(asc(posts.publishedAt), asc(posts.id));\n return rows.map((row) => ({ ...row, commentCount: Number(row.commentCount) }));\n }\n\n async listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(posts.orgId, orgId),\n inArray(posts.spaceId, spaceIds),\n eq(posts.status, \"published\"),\n query.topicId ? eq(posts.topicId, query.topicId) : undefined,\n );\n const rows = await this.db\n .select()\n .from(posts)\n .where(where)\n .orderBy(desc(posts.pinned), desc(posts.publishedAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(posts)\n .where(where);\n return { rows: rows.map(toPost), total, page: query.page, pageSize: query.pageSize };\n }\n\n async listByAuthor(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: PageQuery,\n ): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(posts.orgId, orgId),\n eq(posts.orgUserId, orgUserId),\n inArray(posts.spaceId, spaceIds),\n );\n const rows = await this.db\n .select()\n .from(posts)\n .where(where)\n .orderBy(desc(posts.createdAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(posts)\n .where(where);\n return { rows: rows.map(toPost), total, page: query.page, pageSize: query.pageSize };\n }\n\n async listBookmarked(\n orgId: string,\n orgUserId: string,\n spaceIds: string[],\n query: BookmarkQuery,\n ): Promise<Page<Post>> {\n if (spaceIds.length === 0) {\n return { rows: [], total: 0, page: query.page, pageSize: query.pageSize };\n }\n const where = and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.orgUserId, orgUserId),\n inArray(posts.spaceId, spaceIds),\n or(eq(posts.orgUserId, orgUserId), inArray(posts.status, query.visibleStatuses)),\n );\n const rows = await this.db\n .select({ post: posts })\n .from(postBookmarks)\n .innerJoin(\n posts,\n and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId)),\n )\n .where(where)\n .orderBy(desc(postBookmarks.createdAt), desc(posts.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n const [{ total } = { total: 0 }] = await this.db\n .select({ total: sql<number>`cast(count(*) as int)` })\n .from(postBookmarks)\n .innerJoin(\n posts,\n and(eq(posts.orgId, postBookmarks.orgId), eq(posts.id, postBookmarks.postId)),\n )\n .where(where);\n return {\n rows: rows.map((row) => toPost(row.post)),\n total,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]> {\n const rows = await this.db\n .select()\n .from(posts)\n .where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), eq(posts.status, \"pending\")))\n .orderBy(asc(posts.createdAt), asc(posts.id))\n .limit(limit);\n return rows.map(toPost);\n }\n\n async reactionsOf(\n orgId: string,\n postIds: string[],\n viewerId: string,\n ): Promise<Record<string, PostReactions>> {\n if (postIds.length === 0) {\n return {};\n }\n const rows = await this.db\n .select({\n postId: postReactions.postId,\n emoji: postReactions.emoji,\n count: sql<number>`cast(count(*) as int)`,\n mine: sql<boolean>`bool_or(${postReactions.orgUserId} = ${viewerId})`,\n })\n .from(postReactions)\n .where(and(eq(postReactions.orgId, orgId), inArray(postReactions.postId, postIds)))\n .groupBy(postReactions.postId, postReactions.emoji);\n const byPost: Record<string, PostReactions> = {};\n for (const row of rows) {\n const entry = (byPost[row.postId] ??= { reactions: {} });\n entry.reactions[row.emoji] = row.count;\n if (row.mine) {\n entry.viewerReaction = row.emoji;\n }\n }\n return byPost;\n }\n\n async setReaction(\n orgId: string,\n postId: string,\n orgUserId: string,\n emoji: ReactionEmoji | null,\n ): Promise<boolean> {\n if (emoji === null) {\n await this.db\n .delete(postReactions)\n .where(\n and(\n eq(postReactions.orgId, orgId),\n eq(postReactions.postId, postId),\n eq(postReactions.orgUserId, orgUserId),\n ),\n );\n return false;\n }\n const [row] = await this.db\n .insert(postReactions)\n .values({ orgId, postId, orgUserId, emoji })\n .onConflictDoUpdate({\n target: [postReactions.orgId, postReactions.postId, postReactions.orgUserId],\n set: { emoji },\n })\n .returning({ inserted: insertedRow });\n return row?.inserted === true;\n }\n\n async bookmark(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db.insert(postBookmarks).values({ orgId, postId, orgUserId }).onConflictDoNothing();\n }\n\n async unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void> {\n await this.db\n .delete(postBookmarks)\n .where(\n and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.postId, postId),\n eq(postBookmarks.orgUserId, orgUserId),\n ),\n );\n }\n\n async bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]> {\n if (postIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ postId: postBookmarks.postId })\n .from(postBookmarks)\n .where(\n and(\n eq(postBookmarks.orgId, orgId),\n eq(postBookmarks.orgUserId, orgUserId),\n inArray(postBookmarks.postId, postIds),\n ),\n );\n return rows.map((row) => row.postId);\n }\n\n async insertFlag(orgId: string, flag: PostFlag): Promise<PostFlag | null> {\n const [row] = await this.db\n .insert(postFlags)\n .values({\n orgId,\n id: flag.id,\n postId: flag.postId,\n orgUserId: flag.orgUserId,\n reason: flag.reason,\n resolvedAt: flag.resolvedAt,\n resolution: flag.resolution,\n resolvedBy: flag.resolvedBy,\n createdAt: flag.createdAt,\n updatedAt: flag.updatedAt,\n })\n .onConflictDoNothing()\n .returning();\n return row ? toFlag(row) : null;\n }\n\n async findFlag(orgId: string, postId: string, orgUserId: string): Promise<PostFlag | null> {\n const [row] = await this.db\n .select()\n .from(postFlags)\n .where(\n and(\n eq(postFlags.orgId, orgId),\n eq(postFlags.postId, postId),\n eq(postFlags.orgUserId, orgUserId),\n ),\n )\n .limit(1);\n return row ? toFlag(row) : null;\n }\n\n async listOpenFlags(orgId: string, spaceId: string, limit: number): Promise<PostFlag[]> {\n const rows = await this.db\n .select({ flag: postFlags })\n .from(postFlags)\n .innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId)))\n .where(\n and(eq(postFlags.orgId, orgId), eq(posts.spaceId, spaceId), isNull(postFlags.resolvedAt)),\n )\n .orderBy(asc(postFlags.createdAt), asc(postFlags.id))\n .limit(limit);\n return rows.map((row) => toFlag(row.flag));\n }\n\n async resolveFlags(\n orgId: string,\n postId: string,\n resolved: { resolvedAt: Date; resolvedBy: string; resolution: PostFlagResolution },\n ): Promise<void> {\n await this.db\n .update(postFlags)\n .set({\n resolvedAt: resolved.resolvedAt,\n resolvedBy: resolved.resolvedBy,\n resolution: resolved.resolution,\n })\n .where(\n and(eq(postFlags.orgId, orgId), eq(postFlags.postId, postId), isNull(postFlags.resolvedAt)),\n );\n }\n}\n\ntranslateDbErrors(DrizzlePostRepository);\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger, Subscription, SubscriptionItemType } from "@headless-lms/core/types";
3
+ import { SubscriptionsRepository } from "@headless-lms/core/discussion";
4
+ //#region src/repositories/subscriptions.d.ts
5
+ declare class DrizzleSubscriptionsRepository implements SubscriptionsRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ upsert(orgId: string, subscription: Subscription): Promise<void>;
10
+ remove(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemId: string): Promise<void>;
11
+ forViewer(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[]): Promise<Subscription[]>;
12
+ ofItem(orgId: string, itemType: SubscriptionItemType, itemId: string): Promise<Subscription[]>;
13
+ followerIds(orgId: string, postId: string): Promise<string[]>;
14
+ mutedAmong(orgId: string, itemType: SubscriptionItemType, itemId: string, orgUserIds: string[], at: Date): Promise<string[]>;
15
+ }
16
+ //#endregion
17
+ export { DrizzleSubscriptionsRepository };
18
+ //# sourceMappingURL=subscriptions.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { subscriptions } from "../schema/discussion.js";
2
+ import "../schema/index.js";
3
+ import { and, eq, gt, inArray, isNull, or } from "drizzle-orm";
4
+ import { noopLogger } from "@headless-lms/core/shared/logger";
5
+ //#region src/repositories/subscriptions.ts
6
+ function toSubscription(row) {
7
+ return {
8
+ orgId: row.orgId,
9
+ orgUserId: row.orgUserId,
10
+ itemType: row.itemType,
11
+ itemId: row.itemId,
12
+ state: row.state,
13
+ mutedUntil: row.mutedUntil,
14
+ createdAt: row.createdAt
15
+ };
16
+ }
17
+ var DrizzleSubscriptionsRepository = class {
18
+ db;
19
+ logger;
20
+ constructor(db, logger = noopLogger) {
21
+ this.db = db;
22
+ this.logger = logger;
23
+ }
24
+ async upsert(orgId, subscription) {
25
+ await this.db.insert(subscriptions).values({
26
+ orgId,
27
+ orgUserId: subscription.orgUserId,
28
+ itemType: subscription.itemType,
29
+ itemId: subscription.itemId,
30
+ state: subscription.state,
31
+ mutedUntil: subscription.mutedUntil,
32
+ createdAt: subscription.createdAt
33
+ }).onConflictDoUpdate({
34
+ target: [
35
+ subscriptions.orgId,
36
+ subscriptions.orgUserId,
37
+ subscriptions.itemType,
38
+ subscriptions.itemId
39
+ ],
40
+ set: {
41
+ state: subscription.state,
42
+ mutedUntil: subscription.mutedUntil
43
+ }
44
+ });
45
+ this.logger.debug("subscription stored", {
46
+ orgId,
47
+ itemType: subscription.itemType,
48
+ itemId: subscription.itemId
49
+ });
50
+ }
51
+ async remove(orgId, orgUserId, itemType, itemId) {
52
+ await this.db.delete(subscriptions).where(and(eq(subscriptions.orgId, orgId), eq(subscriptions.orgUserId, orgUserId), eq(subscriptions.itemType, itemType), eq(subscriptions.itemId, itemId)));
53
+ }
54
+ async forViewer(orgId, orgUserId, itemType, itemIds) {
55
+ if (itemIds.length === 0) return [];
56
+ return (await this.db.select().from(subscriptions).where(and(eq(subscriptions.orgId, orgId), eq(subscriptions.orgUserId, orgUserId), eq(subscriptions.itemType, itemType), inArray(subscriptions.itemId, itemIds)))).map(toSubscription);
57
+ }
58
+ async ofItem(orgId, itemType, itemId) {
59
+ return (await this.db.select().from(subscriptions).where(and(eq(subscriptions.orgId, orgId), eq(subscriptions.itemType, itemType), eq(subscriptions.itemId, itemId)))).map(toSubscription);
60
+ }
61
+ async followerIds(orgId, postId) {
62
+ return (await this.db.select({ orgUserId: subscriptions.orgUserId }).from(subscriptions).where(and(eq(subscriptions.orgId, orgId), eq(subscriptions.itemType, "post"), eq(subscriptions.itemId, postId), eq(subscriptions.state, "following")))).map((row) => row.orgUserId);
63
+ }
64
+ async mutedAmong(orgId, itemType, itemId, orgUserIds, at) {
65
+ if (orgUserIds.length === 0) return [];
66
+ return (await this.db.select({ orgUserId: subscriptions.orgUserId }).from(subscriptions).where(and(eq(subscriptions.orgId, orgId), eq(subscriptions.itemType, itemType), eq(subscriptions.itemId, itemId), eq(subscriptions.state, "muted"), inArray(subscriptions.orgUserId, orgUserIds), or(isNull(subscriptions.mutedUntil), gt(subscriptions.mutedUntil, at))))).map((row) => row.orgUserId);
67
+ }
68
+ };
69
+ //#endregion
70
+ export { DrizzleSubscriptionsRepository };
71
+
72
+ //# sourceMappingURL=subscriptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions.js","names":[],"sources":["../../src/repositories/subscriptions.ts"],"sourcesContent":["import { and, eq, gt, inArray, isNull, or } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { SubscriptionsRepository } from \"@headless-lms/core/discussion\";\nimport type { Logger, Subscription, SubscriptionItemType } from \"@headless-lms/core/types\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { subscriptions } from \"../schema/index.js\";\n\ntype SubscriptionRow = typeof subscriptions.$inferSelect;\n\nfunction toSubscription(row: SubscriptionRow): Subscription {\n return {\n orgId: row.orgId,\n orgUserId: row.orgUserId,\n itemType: row.itemType,\n itemId: row.itemId,\n state: row.state,\n mutedUntil: row.mutedUntil,\n createdAt: row.createdAt,\n };\n}\n\nexport class DrizzleSubscriptionsRepository implements SubscriptionsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async upsert(orgId: string, subscription: Subscription): Promise<void> {\n await this.db\n .insert(subscriptions)\n .values({\n orgId,\n orgUserId: subscription.orgUserId,\n itemType: subscription.itemType,\n itemId: subscription.itemId,\n state: subscription.state,\n mutedUntil: subscription.mutedUntil,\n createdAt: subscription.createdAt,\n })\n .onConflictDoUpdate({\n target: [\n subscriptions.orgId,\n subscriptions.orgUserId,\n subscriptions.itemType,\n subscriptions.itemId,\n ],\n set: { state: subscription.state, mutedUntil: subscription.mutedUntil },\n });\n this.logger.debug(\"subscription stored\", {\n orgId,\n itemType: subscription.itemType,\n itemId: subscription.itemId,\n });\n }\n\n async remove(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemId: string,\n ): Promise<void> {\n await this.db\n .delete(subscriptions)\n .where(\n and(\n eq(subscriptions.orgId, orgId),\n eq(subscriptions.orgUserId, orgUserId),\n eq(subscriptions.itemType, itemType),\n eq(subscriptions.itemId, itemId),\n ),\n );\n }\n\n async forViewer(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemIds: string[],\n ): Promise<Subscription[]> {\n if (itemIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(subscriptions)\n .where(\n and(\n eq(subscriptions.orgId, orgId),\n eq(subscriptions.orgUserId, orgUserId),\n eq(subscriptions.itemType, itemType),\n inArray(subscriptions.itemId, itemIds),\n ),\n );\n return rows.map(toSubscription);\n }\n\n async ofItem(\n orgId: string,\n itemType: SubscriptionItemType,\n itemId: string,\n ): Promise<Subscription[]> {\n const rows = await this.db\n .select()\n .from(subscriptions)\n .where(\n and(\n eq(subscriptions.orgId, orgId),\n eq(subscriptions.itemType, itemType),\n eq(subscriptions.itemId, itemId),\n ),\n );\n return rows.map(toSubscription);\n }\n\n async followerIds(orgId: string, postId: string): Promise<string[]> {\n const rows = await this.db\n .select({ orgUserId: subscriptions.orgUserId })\n .from(subscriptions)\n .where(\n and(\n eq(subscriptions.orgId, orgId),\n eq(subscriptions.itemType, \"post\"),\n eq(subscriptions.itemId, postId),\n eq(subscriptions.state, \"following\"),\n ),\n );\n return rows.map((row) => row.orgUserId);\n }\n\n async mutedAmong(\n orgId: string,\n itemType: SubscriptionItemType,\n itemId: string,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ orgUserId: subscriptions.orgUserId })\n .from(subscriptions)\n .where(\n and(\n eq(subscriptions.orgId, orgId),\n eq(subscriptions.itemType, itemType),\n eq(subscriptions.itemId, itemId),\n eq(subscriptions.state, \"muted\"),\n inArray(subscriptions.orgUserId, orgUserIds),\n or(isNull(subscriptions.mutedUntil), gt(subscriptions.mutedUntil, at)),\n ),\n );\n return rows.map((row) => row.orgUserId);\n }\n}\n"],"mappings":"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"}