@headless-lms/adapter-db 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/index.d.ts +7 -6
  2. package/dist/index.js +7 -6
  3. package/dist/repositories/admin.d.ts +23 -0
  4. package/dist/repositories/admin.js +393 -0
  5. package/dist/repositories/admin.js.map +1 -0
  6. package/dist/repositories/assets.d.ts +6 -2
  7. package/dist/repositories/assets.js +50 -9
  8. package/dist/repositories/assets.js.map +1 -1
  9. package/dist/repositories/automations.d.ts +6 -2
  10. package/dist/repositories/automations.js +45 -2
  11. package/dist/repositories/automations.js.map +1 -1
  12. package/dist/repositories/chats.d.ts +55 -0
  13. package/dist/repositories/chats.js +454 -0
  14. package/dist/repositories/chats.js.map +1 -0
  15. package/dist/repositories/content.d.ts +24 -1
  16. package/dist/repositories/content.js +293 -9
  17. package/dist/repositories/content.js.map +1 -1
  18. package/dist/repositories/discussion.d.ts +10 -3
  19. package/dist/repositories/discussion.js +70 -15
  20. package/dist/repositories/discussion.js.map +1 -1
  21. package/dist/repositories/entitlements.d.ts +29 -9
  22. package/dist/repositories/entitlements.js +207 -17
  23. package/dist/repositories/entitlements.js.map +1 -1
  24. package/dist/repositories/grant-covers.d.ts +20 -0
  25. package/dist/repositories/grant-covers.js +55 -0
  26. package/dist/repositories/grant-covers.js.map +1 -0
  27. package/dist/repositories/identity.d.ts +4 -2
  28. package/dist/repositories/identity.js +32 -1
  29. package/dist/repositories/identity.js.map +1 -1
  30. package/dist/repositories/member.d.ts +16 -0
  31. package/dist/repositories/member.js +63 -0
  32. package/dist/repositories/member.js.map +1 -0
  33. package/dist/repositories/members.js +2 -2
  34. package/dist/repositories/members.js.map +1 -1
  35. package/dist/repositories/notifications.d.ts +23 -0
  36. package/dist/repositories/notifications.js +70 -0
  37. package/dist/repositories/notifications.js.map +1 -0
  38. package/dist/repositories/org-user-profile.d.ts +214 -1
  39. package/dist/repositories/org-user-profile.js +23 -2
  40. package/dist/repositories/org-user-profile.js.map +1 -1
  41. package/dist/repositories/organizations.d.ts +3 -2
  42. package/dist/repositories/organizations.js +20 -8
  43. package/dist/repositories/organizations.js.map +1 -1
  44. package/dist/repositories/pg-errors.js +3 -1
  45. package/dist/repositories/pg-errors.js.map +1 -1
  46. package/dist/repositories/posts.d.ts +39 -0
  47. package/dist/repositories/posts.js +255 -0
  48. package/dist/repositories/posts.js.map +1 -0
  49. package/dist/schema/assets.d.ts +19 -2
  50. package/dist/schema/assets.js +10 -4
  51. package/dist/schema/assets.js.map +1 -1
  52. package/dist/schema/automations.d.ts +38 -0
  53. package/dist/schema/automations.js +5 -1
  54. package/dist/schema/automations.js.map +1 -1
  55. package/dist/schema/better-auth.d.ts +1613 -238
  56. package/dist/schema/better-auth.js +107 -22
  57. package/dist/schema/better-auth.js.map +1 -1
  58. package/dist/schema/content.d.ts +642 -5
  59. package/dist/schema/content.js +119 -10
  60. package/dist/schema/content.js.map +1 -1
  61. package/dist/schema/discussion.d.ts +2045 -55
  62. package/dist/schema/discussion.js +426 -11
  63. package/dist/schema/discussion.js.map +1 -1
  64. package/dist/schema/entitlements.d.ts +432 -1
  65. package/dist/schema/entitlements.js +109 -4
  66. package/dist/schema/entitlements.js.map +1 -1
  67. package/dist/schema/identity.d.ts +517 -1
  68. package/dist/schema/identity.js +118 -2
  69. package/dist/schema/identity.js.map +1 -1
  70. package/dist/schema/index.d.ts +6 -6
  71. package/dist/schema/index.js +30 -7
  72. package/dist/schema/organizations.d.ts +4 -4
  73. package/dist/schema/organizations.js +2 -0
  74. package/dist/schema/organizations.js.map +1 -1
  75. package/drizzle/0003_community.sql +340 -0
  76. package/drizzle/0004_assets_sources.sql +1 -0
  77. package/drizzle/0005_notices_and_seed_index.sql +12 -0
  78. package/drizzle/0006_community_review_fixes.sql +24 -0
  79. package/drizzle/0007_notifications.sql +37 -0
  80. package/drizzle/0008_content_access.sql +13 -0
  81. package/drizzle/0009_community_indexes.sql +56 -0
  82. package/drizzle/0010_mcp_access.sql +114 -0
  83. package/drizzle/meta/0003_snapshot.json +7061 -0
  84. package/drizzle/meta/0004_snapshot.json +7068 -0
  85. package/drizzle/meta/0005_snapshot.json +7180 -0
  86. package/drizzle/meta/0006_snapshot.json +7298 -0
  87. package/drizzle/meta/0007_snapshot.json +7620 -0
  88. package/drizzle/meta/0008_snapshot.json +7666 -0
  89. package/drizzle/meta/0009_snapshot.json +8185 -0
  90. package/drizzle/meta/0010_snapshot.json +8813 -0
  91. package/drizzle/meta/_journal.json +57 -1
  92. package/package.json +2 -2
  93. package/dist/repositories/course-analytics.d.ts +0 -15
  94. package/dist/repositories/course-analytics.js +0 -137
  95. package/dist/repositories/course-analytics.js.map +0 -1
  96. package/dist/repositories/dashboard.d.ts +0 -14
  97. package/dist/repositories/dashboard.js +0 -57
  98. package/dist/repositories/dashboard.js.map +0 -1
  99. package/dist/repositories/learn.d.ts +0 -19
  100. package/dist/repositories/learn.js +0 -56
  101. package/dist/repositories/learn.js.map +0 -1
  102. package/dist/repositories/students.d.ts +0 -16
  103. package/dist/repositories/students.js +0 -160
  104. package/dist/repositories/students.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","names":[],"sources":["../../src/repositories/identity.ts"],"sourcesContent":["// identity — Drizzle repository (implements the core outbound port).\n//\n// The person only. A person's link to an organization lives in the\n// organizations context (`org_users`), not here.\nimport { eq, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { IdentityRepository } from \"@headless-lms/core/identity\";\nimport type { CreateUserInput, Logger, UpdateUserInput, User } from \"@headless-lms/core/types\";\nimport { users } from \"../schema/identity.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nexport class DrizzleIdentityRepository implements IdentityRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insertUser(input: CreateUserInput): Promise<User> {\n const [row] = await this.db\n .insert(users)\n .values({\n id: input.id,\n externalId: input.externalId ?? null,\n email: input.email,\n firstName: input.firstName ?? null,\n lastName: input.lastName ?? null,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert user\");\n }\n return row;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User | null> {\n // An empty patch would be an UPDATE with no SET — read the row back instead.\n if (Object.keys(input).length === 0) {\n return this.findUserById(id);\n }\n const [row] = await this.db.update(users).set(input).where(eq(users.id, id)).returning();\n return row ?? null;\n }\n\n async findUserById(id: string): Promise<User | null> {\n const [row] = await this.db.select().from(users).where(eq(users.id, id)).limit(1);\n return row ?? null;\n }\n\n async findUserByExternalId(externalId: string): Promise<User | null> {\n const [row] = await this.db\n .select()\n .from(users)\n .where(eq(users.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n // Case-insensitive: the address comes from whatever an admin typed into an\n // invite, the row from whatever the auth provider stored.\n async findUserByEmail(email: string): Promise<User | null> {\n const [row] = await this.db\n .select()\n .from(users)\n .where(sql`lower(${users.email}) = lower(${email})`)\n .limit(1);\n return row ?? null;\n }\n}\ntranslateDbErrors(DrizzleIdentityRepository);\n"],"mappings":";;;;;AAYA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,WAAW,OAAuC;EACtD,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,KAAK,CAAC,CACb,OAAO;GACN,IAAI,MAAM;GACV,YAAY,MAAM,cAAc;GAChC,OAAO,MAAM;GACb,WAAW,MAAM,aAAa;GAC9B,UAAU,MAAM,YAAY;EAC9B,CAAC,CAAC,CACD,UAAU;EACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,uBAAuB;EAEzC,OAAO;CACT;CAEA,MAAM,WAAW,IAAY,OAA8C;EAEzE,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,GAChC,OAAO,KAAK,aAAa,EAAE;EAE7B,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU;EACvF,OAAO,OAAO;CAChB;CAEA,MAAM,aAAa,IAAkC;EACnD,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;EAChF,OAAO,OAAO;CAChB;CAEA,MAAM,qBAAqB,YAA0C;EACnE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,KAAK,CAAC,CACX,MAAM,GAAG,MAAM,YAAY,UAAU,CAAC,CAAC,CACvC,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;CAIA,MAAM,gBAAgB,OAAqC;EACzD,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,KAAK,CAAC,CACX,MAAM,GAAG,SAAS,MAAM,MAAM,YAAY,MAAM,EAAE,CAAC,CACnD,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;AACF;AACA,kBAAkB,yBAAyB"}
1
+ {"version":3,"file":"identity.js","names":[],"sources":["../../src/repositories/identity.ts"],"sourcesContent":["// identity — Drizzle repository (implements the core outbound port).\n//\n// The person only. A person's link to an organization lives in the\n// organizations context (`org_users`), not here.\nimport { eq, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { IdentityRepository, ProfilePatch } from \"@headless-lms/core/identity\";\nimport type {\n CreateUserInput,\n Logger,\n UpdateUserInput,\n User,\n UserProfile,\n} from \"@headless-lms/core/types\";\nimport { userProfiles, users } from \"../schema/identity.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nexport class DrizzleIdentityRepository implements IdentityRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insertUser(input: CreateUserInput): Promise<User> {\n const [row] = await this.db\n .insert(users)\n .values({\n id: input.id,\n externalId: input.externalId ?? null,\n email: input.email,\n firstName: input.firstName ?? null,\n lastName: input.lastName ?? null,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert user\");\n }\n return row;\n }\n\n async updateUser(id: string, input: UpdateUserInput): Promise<User | null> {\n // An empty patch would be an UPDATE with no SET — read the row back instead.\n if (Object.keys(input).length === 0) {\n return this.findUserById(id);\n }\n const [row] = await this.db.update(users).set(input).where(eq(users.id, id)).returning();\n return row ?? null;\n }\n\n async findUserById(id: string): Promise<User | null> {\n const [row] = await this.db.select().from(users).where(eq(users.id, id)).limit(1);\n return row ?? null;\n }\n\n async findUserByExternalId(externalId: string): Promise<User | null> {\n const [row] = await this.db\n .select()\n .from(users)\n .where(eq(users.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n // Case-insensitive: the address comes from whatever an admin typed into an\n // invite, the row from whatever the auth provider stored.\n async findUserByEmail(email: string): Promise<User | null> {\n const [row] = await this.db\n .select()\n .from(users)\n .where(sql`lower(${users.email}) = lower(${email})`)\n .limit(1);\n return row ?? null;\n }\n\n async findProfile(userId: string): Promise<UserProfile | null> {\n const [row] = await this.db\n .select()\n .from(userProfiles)\n .where(eq(userProfiles.userId, userId))\n .limit(1);\n return row ?? null;\n }\n\n async upsertProfile(userId: string, patch: ProfilePatch): Promise<UserProfile> {\n const existing = await this.findProfile(userId);\n if (!existing) {\n const [row] = await this.db\n .insert(userProfiles)\n .values({\n userId,\n displayName: patch.displayName ?? null,\n headline: patch.headline ?? null,\n bio: patch.bio ?? null,\n imageAssetOrgId: patch.imageAssetOrgId ?? null,\n imageAssetId: patch.imageAssetId ?? null,\n extra: patch.extra ?? {},\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert profile\");\n }\n return row;\n }\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.displayName !== undefined) {\n set.displayName = patch.displayName;\n }\n if (patch.headline !== undefined) {\n set.headline = patch.headline;\n }\n if (patch.bio !== undefined) {\n set.bio = patch.bio;\n }\n if (patch.imageAssetOrgId !== undefined) {\n set.imageAssetOrgId = patch.imageAssetOrgId;\n set.imageAssetId = patch.imageAssetId;\n }\n if (patch.extra !== undefined) {\n set.extra = patch.extra;\n }\n const [row] = await this.db\n .update(userProfiles)\n .set(set)\n .where(eq(userProfiles.userId, userId))\n .returning();\n if (!row) {\n throw new Error(\"failed to update profile\");\n }\n return row;\n }\n}\ntranslateDbErrors(DrizzleIdentityRepository);\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger } from "@headless-lms/core/shared/ports";
3
+ import { Page } from "@headless-lms/core/types";
4
+ import { DirectoryQuery, DirectoryRow, MemberReportRepository } from "@headless-lms/core/reporting/member";
5
+ //#region src/repositories/member.d.ts
6
+ declare class DrizzleMemberRepository implements MemberReportRepository {
7
+ private readonly db;
8
+ private readonly logger;
9
+ constructor(db: NodePgDatabase, logger?: Logger);
10
+ directory(orgId: string, communityId: string, query: DirectoryQuery): Promise<Page<DirectoryRow>>;
11
+ people(orgId: string, orgUserIds: string[]): Promise<DirectoryRow[]>;
12
+ spaceMemberCounts(orgId: string, communityId: string): Promise<Record<string, number>>;
13
+ }
14
+ //#endregion
15
+ export { DrizzleMemberRepository };
16
+ //# sourceMappingURL=member.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { spaces } from "../schema/content.js";
2
+ import { userProfiles, users } from "../schema/identity.js";
3
+ import { orgUsers } from "../schema/organizations.js";
4
+ import { contentAccess, spaceMembers } from "../schema/entitlements.js";
5
+ import "../schema/index.js";
6
+ import { translateDbErrors } from "./pg-errors.js";
7
+ import { communityMember, communityPublished } from "./grant-covers.js";
8
+ import { and, asc, eq, ilike, inArray, sql } from "drizzle-orm";
9
+ import { noopLogger } from "@headless-lms/core/shared/logger";
10
+ //#region src/repositories/member.ts
11
+ function personColumns(orgId) {
12
+ return {
13
+ orgUserId: orgUsers.id,
14
+ userId: orgUsers.userId,
15
+ role: orgUsers.role,
16
+ displayName: sql`coalesce(nullif(trim(${userProfiles.displayName}), ''), nullif(trim(concat_ws(' ', ${users.firstName}, ${users.lastName})), ''))`,
17
+ headline: userProfiles.headline,
18
+ bio: userProfiles.bio,
19
+ imageAssetId: sql`case when ${userProfiles.imageAssetOrgId} = ${orgId} then ${userProfiles.imageAssetId} end`
20
+ };
21
+ }
22
+ var DrizzleMemberRepository = class {
23
+ db;
24
+ logger;
25
+ constructor(db, logger = noopLogger) {
26
+ this.db = db;
27
+ this.logger = logger;
28
+ }
29
+ async directory(orgId, communityId, query) {
30
+ const columns = personColumns(orgId);
31
+ const q = query.search?.trim();
32
+ const where = and(eq(orgUsers.orgId, orgId), communityMember(sql`${orgUsers.orgId}`, sql`${orgUsers.id}`, sql`${orgUsers.role}`, sql`${communityId}`), q ? ilike(columns.displayName, `%${q}%`) : void 0);
33
+ const rows = await this.db.select(columns).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(where).orderBy(asc(userProfiles.displayName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
34
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(where);
35
+ return {
36
+ rows,
37
+ total,
38
+ page: query.page,
39
+ pageSize: query.pageSize
40
+ };
41
+ }
42
+ async people(orgId, orgUserIds) {
43
+ if (orgUserIds.length === 0) return [];
44
+ return this.db.select(personColumns(orgId)).from(orgUsers).leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId)).leftJoin(users, eq(users.id, orgUsers.userId)).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));
45
+ }
46
+ async spaceMemberCounts(orgId, communityId) {
47
+ const holders = this.db.$with("holders").as(this.db.selectDistinct({ orgUserId: contentAccess.orgUserId }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId), communityPublished(sql`${orgId}`, sql`${communityId}`))));
48
+ const seated = this.db.$with("seated").as(this.db.select({
49
+ spaceId: spaceMembers.spaceId,
50
+ count: sql`cast(count(*) as int)`.as("count")
51
+ }).from(spaceMembers).innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId)).where(and(eq(spaceMembers.orgId, orgId), eq(spaceMembers.communityId, communityId), eq(spaceMembers.status, "active"))).groupBy(spaceMembers.spaceId));
52
+ const rows = await this.db.with(holders, seated).select({
53
+ spaceId: spaces.id,
54
+ count: sql`case when ${spaces.accessRule} = 'all_members' then (select cast(count(*) as int) from ${holders}) else coalesce(${sql.identifier("seated")}.${sql.identifier("count")}, 0) end`
55
+ }).from(spaces).leftJoin(seated, eq(seated.spaceId, spaces.id)).where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)));
56
+ return Object.fromEntries(rows.map((row) => [row.spaceId, Number(row.count)]));
57
+ }
58
+ };
59
+ translateDbErrors(DrizzleMemberRepository);
60
+ //#endregion
61
+ export { DrizzleMemberRepository };
62
+
63
+ //# sourceMappingURL=member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member.js","names":[],"sources":["../../src/repositories/member.ts"],"sourcesContent":["// member — Drizzle read repo (implements the reporting/member outbound port).\nimport { and, asc, eq, ilike, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type {\n DirectoryQuery,\n DirectoryRow,\n MemberReportRepository,\n} from \"@headless-lms/core/reporting/member\";\nimport type { Page } from \"@headless-lms/core/types\";\nimport {\n contentAccess,\n orgUsers,\n spaceMembers,\n spaces,\n userProfiles,\n users,\n} from \"../schema/index.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\nimport { communityMember, communityPublished } from \"./grant-covers.js\";\n\nfunction personColumns(orgId: string) {\n return {\n orgUserId: orgUsers.id,\n userId: orgUsers.userId,\n role: orgUsers.role,\n displayName: sql<\n string | null\n >`coalesce(nullif(trim(${userProfiles.displayName}), ''), nullif(trim(concat_ws(' ', ${users.firstName}, ${users.lastName})), ''))`,\n headline: userProfiles.headline,\n bio: userProfiles.bio,\n imageAssetId: sql<\n string | null\n >`case when ${userProfiles.imageAssetOrgId} = ${orgId} then ${userProfiles.imageAssetId} end`,\n };\n}\n\nexport class DrizzleMemberRepository implements MemberReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async directory(\n orgId: string,\n communityId: string,\n query: DirectoryQuery,\n ): Promise<Page<DirectoryRow>> {\n const columns = personColumns(orgId);\n const q = query.search?.trim();\n const where = and(\n eq(orgUsers.orgId, orgId),\n communityMember(\n sql`${orgUsers.orgId}`,\n sql`${orgUsers.id}`,\n sql`${orgUsers.role}`,\n sql`${communityId}`,\n ),\n q ? ilike(columns.displayName, `%${q}%`) : undefined,\n );\n const rows = await this.db\n .select(columns)\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where)\n .orderBy(asc(userProfiles.displayName), asc(orgUsers.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(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n return { rows, total, page: query.page, pageSize: query.pageSize };\n }\n\n async people(orgId: string, orgUserIds: string[]): Promise<DirectoryRow[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n return this.db\n .select(personColumns(orgId))\n .from(orgUsers)\n .leftJoin(userProfiles, eq(userProfiles.userId, orgUsers.userId))\n .leftJoin(users, eq(users.id, orgUsers.userId))\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, orgUserIds)));\n }\n\n async spaceMemberCounts(orgId: string, communityId: string): Promise<Record<string, number>> {\n const holders = this.db.$with(\"holders\").as(\n this.db\n .selectDistinct({ orgUserId: contentAccess.orgUserId })\n .from(contentAccess)\n .where(\n and(\n eq(contentAccess.orgId, orgId),\n eq(contentAccess.contentId, communityId),\n communityPublished(sql`${orgId}`, sql`${communityId}`),\n ),\n ),\n );\n const seated = this.db.$with(\"seated\").as(\n this.db\n .select({\n spaceId: spaceMembers.spaceId,\n count: sql<number>`cast(count(*) as int)`.as(\"count\"),\n })\n .from(spaceMembers)\n .innerJoin(holders, eq(holders.orgUserId, spaceMembers.orgUserId))\n .where(\n and(\n eq(spaceMembers.orgId, orgId),\n eq(spaceMembers.communityId, communityId),\n eq(spaceMembers.status, \"active\"),\n ),\n )\n .groupBy(spaceMembers.spaceId),\n );\n const rows = await this.db\n .with(holders, seated)\n .select({\n spaceId: spaces.id,\n count: sql<number>`case when ${spaces.accessRule} = 'all_members' then (select cast(count(*) as int) from ${holders}) else coalesce(${sql.identifier(\"seated\")}.${sql.identifier(\"count\")}, 0) end`,\n })\n .from(spaces)\n .leftJoin(seated, eq(seated.spaceId, spaces.id))\n .where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)));\n return Object.fromEntries(rows.map((row) => [row.spaceId, Number(row.count)]));\n }\n}\ntranslateDbErrors(DrizzleMemberRepository);\n"],"mappings":"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"}
@@ -5,9 +5,9 @@ import { user } from "../schema/better-auth.js";
5
5
  import { orgUserProfileColumns } from "./org-user-profile.js";
6
6
  import { and, eq, ne } from "drizzle-orm";
7
7
  import { noopLogger } from "@headless-lms/core/shared/logger";
8
- import { STUDENT_ROLE, isStaffRole } from "@headless-lms/core/organizations";
8
+ import { STUDENT_ROLE, isRole } from "@headless-lms/core/organizations";
9
9
  //#region src/repositories/members.ts
10
- const roleOf = (t) => isStaffRole(t) ? t : "instructor";
10
+ const roleOf = (t) => isRole(t) && t !== STUDENT_ROLE ? t : "instructor";
11
11
  function toMember(r) {
12
12
  return {
13
13
  id: r.id,
@@ -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 isStaffRole,\n STUDENT_ROLE,\n type Member,\n type MemberRecord,\n type MembersQuery,\n type MembersRepository,\n type Page,\n type StaffRole,\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\n// The member surface is staff-only; a student org user never reaches here\n// (the queries below exclude it), so an unrecognized role falls back to the\n// least-privileged staff role rather than widening to the full Role union.\nconst roleOf = (t: string): StaffRole => (isStaffRole(t) ? 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":["// 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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"}
@@ -0,0 +1,23 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Notification, NotificationPreference } from "@headless-lms/core/types";
3
+ import { NotificationsQuery, NotificationsRepository } from "@headless-lms/core/identity";
4
+ //#region src/repositories/notifications.d.ts
5
+ declare class DrizzleNotificationsRepository implements NotificationsRepository {
6
+ private readonly db;
7
+ constructor(db: NodePgDatabase);
8
+ insert(orgId: string, rows: Notification[]): Promise<void>;
9
+ page(orgId: string, userId: string, query: NotificationsQuery): Promise<{
10
+ rows: Notification[];
11
+ total: number;
12
+ unreadCount: number;
13
+ }>;
14
+ markRead(orgId: string, userId: string, notificationId: string, at: Date): Promise<Notification | null>;
15
+ markAllRead(orgId: string, userId: string, at: Date): Promise<number>;
16
+ preferences(orgId: string, userIds: string[]): Promise<(NotificationPreference & {
17
+ userId: string;
18
+ })[]>;
19
+ upsertPreferences(orgId: string, userId: string, preferences: NotificationPreference[]): Promise<void>;
20
+ }
21
+ //#endregion
22
+ export { DrizzleNotificationsRepository };
23
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { notificationPreferences, notifications } from "../schema/identity.js";
2
+ import "../schema/index.js";
3
+ import { translateDbErrors } from "./pg-errors.js";
4
+ import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
5
+ //#region src/repositories/notifications.ts
6
+ var DrizzleNotificationsRepository = class {
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ async insert(orgId, rows) {
12
+ if (rows.length === 0) return;
13
+ await this.db.insert(notifications).values(rows.map((row) => ({
14
+ ...row,
15
+ orgId
16
+ })));
17
+ }
18
+ async page(orgId, userId, query) {
19
+ const mine = and(eq(notifications.orgId, orgId), eq(notifications.userId, userId));
20
+ const [rows, counts] = await Promise.all([this.db.select().from(notifications).where(mine).orderBy(desc(notifications.createdAt), desc(notifications.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize), this.db.select({
21
+ total: sql`count(*)::int`,
22
+ unread: sql`(count(*) filter (where ${notifications.readAt} is null))::int`
23
+ }).from(notifications).where(mine)]);
24
+ return {
25
+ rows,
26
+ total: Number(counts[0]?.total ?? 0),
27
+ unreadCount: Number(counts[0]?.unread ?? 0)
28
+ };
29
+ }
30
+ async markRead(orgId, userId, notificationId, at) {
31
+ const [row] = await this.db.update(notifications).set({ readAt: sql`coalesce(${notifications.readAt}, ${at.toISOString()}::timestamptz)` }).where(and(eq(notifications.orgId, orgId), eq(notifications.userId, userId), eq(notifications.id, notificationId))).returning();
32
+ return row ?? null;
33
+ }
34
+ async markAllRead(orgId, userId, at) {
35
+ return (await this.db.update(notifications).set({ readAt: at }).where(and(eq(notifications.orgId, orgId), eq(notifications.userId, userId), isNull(notifications.readAt))).returning({ id: notifications.id })).length;
36
+ }
37
+ async preferences(orgId, userIds) {
38
+ if (userIds.length === 0) return [];
39
+ return this.db.select({
40
+ userId: notificationPreferences.userId,
41
+ kind: notificationPreferences.kind,
42
+ inApp: notificationPreferences.inApp,
43
+ email: notificationPreferences.email
44
+ }).from(notificationPreferences).where(and(eq(notificationPreferences.orgId, orgId), inArray(notificationPreferences.userId, userIds)));
45
+ }
46
+ async upsertPreferences(orgId, userId, preferences) {
47
+ if (preferences.length === 0) return;
48
+ await this.db.insert(notificationPreferences).values(preferences.map((pref) => ({
49
+ orgId,
50
+ userId,
51
+ ...pref
52
+ }))).onConflictDoUpdate({
53
+ target: [
54
+ notificationPreferences.orgId,
55
+ notificationPreferences.userId,
56
+ notificationPreferences.kind
57
+ ],
58
+ set: {
59
+ inApp: sql`excluded.in_app`,
60
+ email: sql`excluded.email`,
61
+ updatedAt: sql`now()`
62
+ }
63
+ });
64
+ }
65
+ };
66
+ translateDbErrors(DrizzleNotificationsRepository);
67
+ //#endregion
68
+ export { DrizzleNotificationsRepository };
69
+
70
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import { and, desc, eq, inArray, isNull, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { NotificationsQuery, NotificationsRepository } from \"@headless-lms/core/identity\";\nimport type { Notification, NotificationPreference } from \"@headless-lms/core/types\";\nimport { notificationPreferences, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: NodePgDatabase) {}\n\n async insert(orgId: string, rows: Notification[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.insert(notifications).values(rows.map((row) => ({ ...row, orgId })));\n }\n\n async page(\n orgId: string,\n userId: string,\n query: NotificationsQuery,\n ): Promise<{ rows: Notification[]; total: number; unreadCount: number }> {\n const mine = and(eq(notifications.orgId, orgId), eq(notifications.userId, userId));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(notifications)\n .where(mine)\n .orderBy(desc(notifications.createdAt), desc(notifications.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${notifications.readAt} is null))::int`,\n })\n .from(notifications)\n .where(mine),\n ]);\n return {\n rows,\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<Notification | null> {\n const [row] = await this.db\n .update(notifications)\n .set({ readAt: sql`coalesce(${notifications.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(\n eq(notifications.orgId, orgId),\n eq(notifications.userId, userId),\n eq(notifications.id, notificationId),\n ),\n )\n .returning();\n return row ?? null;\n }\n\n async markAllRead(orgId: string, userId: string, at: Date): Promise<number> {\n const rows = await this.db\n .update(notifications)\n .set({ readAt: at })\n .where(\n and(\n eq(notifications.orgId, orgId),\n eq(notifications.userId, userId),\n isNull(notifications.readAt),\n ),\n )\n .returning({ id: notifications.id });\n return rows.length;\n }\n\n async preferences(\n orgId: string,\n userIds: string[],\n ): Promise<(NotificationPreference & { userId: string })[]> {\n if (userIds.length === 0) {\n return [];\n }\n return this.db\n .select({\n userId: notificationPreferences.userId,\n kind: notificationPreferences.kind,\n inApp: notificationPreferences.inApp,\n email: notificationPreferences.email,\n })\n .from(notificationPreferences)\n .where(\n and(\n eq(notificationPreferences.orgId, orgId),\n inArray(notificationPreferences.userId, userIds),\n ),\n );\n }\n\n async upsertPreferences(\n orgId: string,\n userId: string,\n preferences: NotificationPreference[],\n ): Promise<void> {\n if (preferences.length === 0) {\n return;\n }\n await this.db\n .insert(notificationPreferences)\n .values(preferences.map((pref) => ({ orgId, userId, ...pref })))\n .onConflictDoUpdate({\n target: [\n notificationPreferences.orgId,\n notificationPreferences.userId,\n notificationPreferences.kind,\n ],\n set: {\n inApp: sql`excluded.in_app`,\n email: sql`excluded.email`,\n updatedAt: sql`now()`,\n },\n });\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":";;;;;AAOA,IAAa,iCAAb,MAA+E;CAChD;CAA7B,YAAY,IAAqC;EAApB,KAAA,KAAA;CAAqB;CAElD,MAAM,OAAO,OAAe,MAAqC;EAC/D,IAAI,KAAK,WAAW,GAClB;EAEF,MAAM,KAAK,GAAG,OAAO,aAAa,CAAC,CAAC,OAAO,KAAK,KAAK,SAAS;GAAE,GAAG;GAAK;EAAM,EAAE,CAAC;CACnF;CAEA,MAAM,KACJ,OACA,QACA,OACuE;EACvE,MAAM,OAAO,IAAI,GAAG,cAAc,OAAO,KAAK,GAAG,GAAG,cAAc,QAAQ,MAAM,CAAC;EACjF,MAAM,CAAC,MAAM,UAAU,MAAM,QAAQ,IAAI,CACvC,KAAK,GACF,OAAO,CAAC,CACR,KAAK,aAAa,CAAC,CACnB,MAAM,IAAI,CAAC,CACX,QAAQ,KAAK,cAAc,SAAS,GAAG,KAAK,cAAc,EAAE,CAAC,CAAC,CAC9D,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ,GAC3C,KAAK,GACF,OAAO;GACN,OAAO,GAAW;GAClB,QAAQ,GAAW,2BAA2B,cAAc,OAAO;EACrE,CAAC,CAAC,CACD,KAAK,aAAa,CAAC,CACnB,MAAM,IAAI,CACf,CAAC;EACD,OAAO;GACL;GACA,OAAO,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC;GACnC,aAAa,OAAO,OAAO,EAAE,EAAE,UAAU,CAAC;EAC5C;CACF;CAEA,MAAM,SACJ,OACA,QACA,gBACA,IAC8B;EAC9B,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,aAAa,CAAC,CACrB,IAAI,EAAE,QAAQ,GAAG,YAAY,cAAc,OAAO,IAAI,GAAG,YAAY,EAAE,gBAAgB,CAAC,CAAC,CACzF,MACC,IACE,GAAG,cAAc,OAAO,KAAK,GAC7B,GAAG,cAAc,QAAQ,MAAM,GAC/B,GAAG,cAAc,IAAI,cAAc,CACrC,CACF,CAAC,CACA,UAAU;EACb,OAAO,OAAO;CAChB;CAEA,MAAM,YAAY,OAAe,QAAgB,IAA2B;EAY1E,QAAO,MAXY,KAAK,GACrB,OAAO,aAAa,CAAC,CACrB,IAAI,EAAE,QAAQ,GAAG,CAAC,CAAC,CACnB,MACC,IACE,GAAG,cAAc,OAAO,KAAK,GAC7B,GAAG,cAAc,QAAQ,MAAM,GAC/B,OAAO,cAAc,MAAM,CAC7B,CACF,CAAC,CACA,UAAU,EAAE,IAAI,cAAc,GAAG,CAAC,EAAA,CACzB;CACd;CAEA,MAAM,YACJ,OACA,SAC0D;EAC1D,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,OAAO,KAAK,GACT,OAAO;GACN,QAAQ,wBAAwB;GAChC,MAAM,wBAAwB;GAC9B,OAAO,wBAAwB;GAC/B,OAAO,wBAAwB;EACjC,CAAC,CAAC,CACD,KAAK,uBAAuB,CAAC,CAC7B,MACC,IACE,GAAG,wBAAwB,OAAO,KAAK,GACvC,QAAQ,wBAAwB,QAAQ,OAAO,CACjD,CACF;CACJ;CAEA,MAAM,kBACJ,OACA,QACA,aACe;EACf,IAAI,YAAY,WAAW,GACzB;EAEF,MAAM,KAAK,GACR,OAAO,uBAAuB,CAAC,CAC/B,OAAO,YAAY,KAAK,UAAU;GAAE;GAAO;GAAQ,GAAG;EAAK,EAAE,CAAC,CAAC,CAC/D,mBAAmB;GAClB,QAAQ;IACN,wBAAwB;IACxB,wBAAwB;IACxB,wBAAwB;GAC1B;GACA,KAAK;IACH,OAAO,GAAG;IACV,OAAO,GAAG;IACV,WAAW,GAAG;GAChB;EACF,CAAC;CACL;AACF;AAEA,kBAAkB,8BAA8B"}
@@ -87,6 +87,219 @@ declare const orgUserProfileColumns: {
87
87
  generated: undefined;
88
88
  }, {}, {}>;
89
89
  };
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
+ displayName: import("drizzle-orm/pg-core").PgColumn<{
109
+ name: "display_name";
110
+ tableName: "user_profiles";
111
+ dataType: "string";
112
+ columnType: "PgText";
113
+ data: string;
114
+ driverParam: string;
115
+ notNull: false;
116
+ hasDefault: false;
117
+ isPrimaryKey: false;
118
+ isAutoincrement: false;
119
+ hasRuntimeDefault: false;
120
+ enumValues: [string, ...string[]];
121
+ baseColumn: never;
122
+ identity: undefined;
123
+ generated: undefined;
124
+ }, {}, {}>;
125
+ headline: import("drizzle-orm/pg-core").PgColumn<{
126
+ name: "headline";
127
+ tableName: "user_profiles";
128
+ dataType: "string";
129
+ columnType: "PgText";
130
+ data: string;
131
+ driverParam: string;
132
+ notNull: false;
133
+ hasDefault: false;
134
+ isPrimaryKey: false;
135
+ isAutoincrement: false;
136
+ hasRuntimeDefault: false;
137
+ enumValues: [string, ...string[]];
138
+ baseColumn: never;
139
+ identity: undefined;
140
+ generated: undefined;
141
+ }, {}, {}>;
142
+ imageAssetOrgId: import("drizzle-orm/pg-core").PgColumn<{
143
+ name: "image_asset_org_id";
144
+ tableName: "user_profiles";
145
+ dataType: "string";
146
+ columnType: "PgText";
147
+ data: string;
148
+ driverParam: string;
149
+ notNull: false;
150
+ hasDefault: false;
151
+ isPrimaryKey: false;
152
+ isAutoincrement: false;
153
+ hasRuntimeDefault: false;
154
+ enumValues: [string, ...string[]];
155
+ baseColumn: never;
156
+ identity: undefined;
157
+ generated: undefined;
158
+ }, {}, {}>;
159
+ imageAssetId: import("drizzle-orm/pg-core").PgColumn<{
160
+ name: "image_asset_id";
161
+ tableName: "user_profiles";
162
+ dataType: "string";
163
+ columnType: "PgText";
164
+ data: string;
165
+ driverParam: string;
166
+ notNull: false;
167
+ hasDefault: false;
168
+ isPrimaryKey: false;
169
+ isAutoincrement: false;
170
+ hasRuntimeDefault: false;
171
+ enumValues: [string, ...string[]];
172
+ baseColumn: never;
173
+ identity: undefined;
174
+ generated: undefined;
175
+ }, {}, {}>;
176
+ id: import("drizzle-orm/pg-core").PgColumn<{
177
+ name: "id";
178
+ tableName: "org_users";
179
+ dataType: "string";
180
+ columnType: "PgText";
181
+ data: string;
182
+ driverParam: string;
183
+ notNull: true;
184
+ hasDefault: true;
185
+ isPrimaryKey: false;
186
+ isAutoincrement: false;
187
+ hasRuntimeDefault: true;
188
+ enumValues: [string, ...string[]];
189
+ baseColumn: never;
190
+ identity: undefined;
191
+ generated: undefined;
192
+ }, {}, {}>;
193
+ firstName: import("drizzle-orm/pg-core").PgColumn<{
194
+ name: "first_name";
195
+ tableName: "users";
196
+ dataType: "string";
197
+ columnType: "PgText";
198
+ data: string;
199
+ driverParam: string;
200
+ notNull: false;
201
+ hasDefault: false;
202
+ isPrimaryKey: false;
203
+ isAutoincrement: false;
204
+ hasRuntimeDefault: false;
205
+ enumValues: [string, ...string[]];
206
+ baseColumn: never;
207
+ identity: undefined;
208
+ generated: undefined;
209
+ }, {}, {}>;
210
+ lastName: import("drizzle-orm/pg-core").PgColumn<{
211
+ name: "last_name";
212
+ tableName: "users";
213
+ dataType: "string";
214
+ columnType: "PgText";
215
+ data: string;
216
+ driverParam: string;
217
+ notNull: false;
218
+ hasDefault: false;
219
+ isPrimaryKey: false;
220
+ isAutoincrement: false;
221
+ hasRuntimeDefault: false;
222
+ enumValues: [string, ...string[]];
223
+ baseColumn: never;
224
+ identity: undefined;
225
+ generated: undefined;
226
+ }, {}, {}>;
227
+ email: import("drizzle-orm/pg-core").PgColumn<{
228
+ name: "email";
229
+ tableName: "users";
230
+ dataType: "string";
231
+ columnType: "PgText";
232
+ data: string;
233
+ driverParam: string;
234
+ notNull: true;
235
+ hasDefault: false;
236
+ isPrimaryKey: false;
237
+ isAutoincrement: false;
238
+ hasRuntimeDefault: false;
239
+ enumValues: [string, ...string[]];
240
+ baseColumn: never;
241
+ identity: undefined;
242
+ generated: undefined;
243
+ }, {}, {}>;
244
+ image: import("drizzle-orm/pg-core").PgColumn<{
245
+ name: "image";
246
+ tableName: "ba_user";
247
+ dataType: "string";
248
+ columnType: "PgText";
249
+ data: string;
250
+ driverParam: string;
251
+ notNull: false;
252
+ hasDefault: false;
253
+ isPrimaryKey: false;
254
+ isAutoincrement: false;
255
+ hasRuntimeDefault: false;
256
+ enumValues: [string, ...string[]];
257
+ baseColumn: never;
258
+ identity: undefined;
259
+ generated: undefined;
260
+ }, {}, {}>;
261
+ };
262
+ declare const studentFirstName: import("drizzle-orm").SQL<string | null>;
263
+ declare const studentLastName: import("drizzle-orm").SQL<string | null>;
264
+ declare const studentProfileColumns: {
265
+ id: import("drizzle-orm/pg-core").PgColumn<{
266
+ name: "id";
267
+ tableName: "org_users";
268
+ dataType: "string";
269
+ columnType: "PgText";
270
+ data: string;
271
+ driverParam: string;
272
+ notNull: true;
273
+ hasDefault: true;
274
+ isPrimaryKey: false;
275
+ isAutoincrement: false;
276
+ hasRuntimeDefault: true;
277
+ enumValues: [string, ...string[]];
278
+ baseColumn: never;
279
+ identity: undefined;
280
+ generated: undefined;
281
+ }, {}, {}>;
282
+ firstName: import("drizzle-orm").SQL<string | null>;
283
+ lastName: import("drizzle-orm").SQL<string | null>;
284
+ email: import("drizzle-orm/pg-core").PgColumn<{
285
+ name: "email";
286
+ tableName: "users";
287
+ dataType: "string";
288
+ columnType: "PgText";
289
+ data: string;
290
+ driverParam: string;
291
+ notNull: true;
292
+ hasDefault: false;
293
+ isPrimaryKey: false;
294
+ isAutoincrement: false;
295
+ hasRuntimeDefault: false;
296
+ enumValues: [string, ...string[]];
297
+ baseColumn: never;
298
+ identity: undefined;
299
+ generated: undefined;
300
+ }, {}, {}>;
301
+ image: import("drizzle-orm").SQL<string | null>;
302
+ };
90
303
  //#endregion
91
- export { orgUserProfileColumns };
304
+ export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
92
305
  //# sourceMappingURL=org-user-profile.d.ts.map
@@ -1,6 +1,7 @@
1
- import { users } from "../schema/identity.js";
1
+ import { userProfiles, users } from "../schema/identity.js";
2
2
  import { orgUsers } from "../schema/organizations.js";
3
3
  import { user } from "../schema/better-auth.js";
4
+ import { sql } from "drizzle-orm";
4
5
  //#region src/repositories/org-user-profile.ts
5
6
  /** Spread into a `.select({ ... })` to get the profile columns. */
6
7
  const orgUserProfileColumns = {
@@ -10,7 +11,27 @@ const orgUserProfileColumns = {
10
11
  email: users.email,
11
12
  image: user.image
12
13
  };
14
+ const authorProfileColumns = {
15
+ ...orgUserProfileColumns,
16
+ role: orgUsers.role,
17
+ displayName: userProfiles.displayName,
18
+ headline: userProfiles.headline,
19
+ imageAssetOrgId: userProfiles.imageAssetOrgId,
20
+ imageAssetId: userProfiles.imageAssetId
21
+ };
22
+ const profileVisible = sql`(${orgUsers.status} <> 'invited' or (${users.externalId} is null and not exists (
23
+ select 1 from org_users other where other.user_id = ${users.id} and other.org_id <> ${orgUsers.orgId}
24
+ )))`;
25
+ const studentFirstName = sql`case when ${profileVisible} then ${users.firstName} end`;
26
+ const studentLastName = sql`case when ${profileVisible} then ${users.lastName} end`;
27
+ const studentProfileColumns = {
28
+ id: orgUsers.id,
29
+ firstName: studentFirstName,
30
+ lastName: studentLastName,
31
+ email: users.email,
32
+ image: sql`case when ${profileVisible} then ${user.image} end`
33
+ };
13
34
  //#endregion
14
- export { orgUserProfileColumns };
35
+ export { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns };
15
36
 
16
37
  //# sourceMappingURL=org-user-profile.js.map
@@ -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 { orgUsers } from \"../schema/organizations.js\";\nimport { 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"],"mappings":";;;;;AAmBA,MAAa,wBAAwB;CACnC,IAAI,SAAS;CACb,WAAW,MAAM;CACjB,UAAU,MAAM;CAChB,OAAO,MAAM;CACb,OAAO,KAAK;AACd"}
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,5 +1,5 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, UpdateOrgUserInput, UpdateOrganizationInput } from "@headless-lms/core/organizations";
2
+ import { CreateOrgUserInput, Invite, NewInviteRow, NewOrganizationRow, OrgUser, Organization, OrganizationsRepository, Role, 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 {
@@ -20,10 +20,11 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
20
20
  findOrgUser(orgId: string, userId: string): Promise<OrgUser | null>;
21
21
  findOrgUsersByUser(userId: string): Promise<OrgUser[]>;
22
22
  findOrgUserById(orgId: string, id: string): Promise<OrgUser | null>;
23
+ findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]>;
24
+ findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]>;
23
25
  createOrgUser(input: CreateOrgUserInput): Promise<OrgUser>;
24
26
  updateOrgUser(orgId: string, id: string, patch: UpdateOrgUserInput): Promise<OrgUser | null>;
25
27
  findStudentOrgUsers(userId: string): Promise<OrgUser[]>;
26
- cancelPendingInvite(orgId: string, email: string): Promise<Invite | null>;
27
28
  deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null>;
28
29
  }
29
30
  //#endregion
@@ -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, 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";
@@ -129,6 +129,17 @@ var DrizzleOrganizationsRepository = class {
129
129
  role: parseRole(row.role)
130
130
  } : null;
131
131
  }
132
+ async findOrgUsersByIds(orgId, ids) {
133
+ if (ids.length === 0) return [];
134
+ return (await this.db.select().from(orgUsers).where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)))).map((row) => ({
135
+ ...row,
136
+ role: parseRole(row.role)
137
+ }));
138
+ }
139
+ async findOrgUserIdsByRole(orgId, roles) {
140
+ if (roles.length === 0) return [];
141
+ 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);
142
+ }
132
143
  async createOrgUser(input) {
133
144
  try {
134
145
  const [row] = await this.db.insert(orgUsers).values({
@@ -163,13 +174,14 @@ var DrizzleOrganizationsRepository = class {
163
174
  role: parseRole(row.role)
164
175
  }));
165
176
  }
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
177
  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;
178
+ try {
179
+ const [row] = await this.db.delete(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id))).returning();
180
+ return row ?? null;
181
+ } catch (err) {
182
+ if (pgErrorFields(err)?.code === "23503") throw Object.assign(new ConflictError("This person has written posts, comments or messages and cannot be deleted"), { cause: err });
183
+ throw err;
184
+ }
173
185
  }
174
186
  };
175
187
  translateDbErrors(DrizzleOrganizationsRepository);