@headless-lms/adapter-db 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +8 -6
- package/dist/migrate.js.map +1 -1
- package/dist/repositories/admin.d.ts +23 -0
- package/dist/repositories/admin.js +393 -0
- package/dist/repositories/admin.js.map +1 -0
- package/dist/repositories/assets.d.ts +6 -2
- package/dist/repositories/assets.js +53 -9
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +12 -6
- package/dist/repositories/automations.js +79 -11
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +55 -0
- package/dist/repositories/chats.js +454 -0
- package/dist/repositories/chats.js.map +1 -0
- package/dist/repositories/content.d.ts +44 -7
- package/dist/repositories/content.js +458 -24
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/credentials.js +2 -0
- package/dist/repositories/credentials.js.map +1 -1
- package/dist/repositories/discussion.d.ts +11 -4
- package/dist/repositories/discussion.js +72 -15
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +36 -10
- package/dist/repositories/entitlements.js +238 -23
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/grant-covers.d.ts +20 -0
- package/dist/repositories/grant-covers.js +55 -0
- package/dist/repositories/grant-covers.js.map +1 -0
- package/dist/repositories/identity.d.ts +4 -2
- package/dist/repositories/identity.js +34 -1
- package/dist/repositories/identity.js.map +1 -1
- package/dist/repositories/integrations.d.ts +1 -1
- package/dist/repositories/integrations.js +2 -0
- package/dist/repositories/integrations.js.map +1 -1
- package/dist/repositories/member.d.ts +16 -0
- package/dist/repositories/member.js +63 -0
- package/dist/repositories/member.js.map +1 -0
- package/dist/repositories/members.js +4 -2
- package/dist/repositories/members.js.map +1 -1
- package/dist/repositories/notifications.d.ts +23 -0
- package/dist/repositories/notifications.js +70 -0
- package/dist/repositories/notifications.js.map +1 -0
- package/dist/repositories/org-user-profile.d.ts +214 -1
- package/dist/repositories/org-user-profile.js +23 -2
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +6 -7
- package/dist/repositories/organizations.js +45 -52
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/outbox.js +3 -0
- package/dist/repositories/outbox.js.map +1 -1
- package/dist/repositories/pg-errors.d.ts +53 -5
- package/dist/repositories/pg-errors.js +99 -8
- package/dist/repositories/pg-errors.js.map +1 -1
- package/dist/repositories/posts.d.ts +39 -0
- package/dist/repositories/posts.js +255 -0
- package/dist/repositories/posts.js.map +1 -0
- package/dist/repositories/progress.d.ts +1 -1
- package/dist/repositories/progress.js +3 -0
- package/dist/repositories/progress.js.map +1 -1
- package/dist/repositories/settings.js +2 -0
- package/dist/repositories/settings.js.map +1 -1
- package/dist/schema/assets.d.ts +36 -2
- package/dist/schema/assets.js +12 -5
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +123 -8
- package/dist/schema/automations.js +11 -4
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/better-auth.d.ts +1613 -238
- package/dist/schema/better-auth.js +107 -22
- package/dist/schema/better-auth.js.map +1 -1
- package/dist/schema/content.d.ts +909 -22
- package/dist/schema/content.js +157 -13
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/credentials.js +1 -1
- package/dist/schema/credentials.js.map +1 -1
- package/dist/schema/discussion.d.ts +2079 -55
- package/dist/schema/discussion.js +430 -13
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/entitlements.d.ts +484 -2
- package/dist/schema/entitlements.js +121 -6
- package/dist/schema/entitlements.js.map +1 -1
- package/dist/schema/identity.d.ts +517 -1
- package/dist/schema/identity.js +118 -2
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +6 -6
- package/dist/schema/index.js +32 -7
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/integrations.js +1 -1
- package/dist/schema/integrations.js.map +1 -1
- package/dist/schema/organizations.d.ts +21 -4
- package/dist/schema/organizations.js +4 -1
- package/dist/schema/organizations.js.map +1 -1
- package/dist/schema/outbox.d.ts +17 -0
- package/dist/schema/outbox.js +1 -0
- package/dist/schema/outbox.js.map +1 -1
- package/dist/schema/progress.d.ts +17 -0
- package/dist/schema/progress.js +1 -0
- package/dist/schema/progress.js.map +1 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/unit-of-work.js +7 -2
- package/dist/unit-of-work.js.map +1 -1
- package/drizzle/0000_baseline.sql +0 -1
- package/drizzle/0001_new_sentry.sql +43 -0
- package/drizzle/0002_automation_kinds.sql +7 -0
- package/drizzle/0003_community.sql +340 -0
- package/drizzle/0004_assets_sources.sql +1 -0
- package/drizzle/0005_notices_and_seed_index.sql +12 -0
- package/drizzle/0006_community_review_fixes.sql +24 -0
- package/drizzle/0007_notifications.sql +37 -0
- package/drizzle/0008_content_access.sql +13 -0
- package/drizzle/0009_community_indexes.sql +56 -0
- package/drizzle/0010_mcp_access.sql +114 -0
- package/drizzle/meta/0000_snapshot.json +167 -597
- package/drizzle/meta/0001_snapshot.json +3769 -0
- package/drizzle/meta/0002_snapshot.json +3770 -0
- package/drizzle/meta/0003_snapshot.json +7061 -0
- package/drizzle/meta/0004_snapshot.json +7068 -0
- package/drizzle/meta/0005_snapshot.json +7180 -0
- package/drizzle/meta/0006_snapshot.json +7298 -0
- package/drizzle/meta/0007_snapshot.json +7620 -0
- package/drizzle/meta/0008_snapshot.json +7666 -0
- package/drizzle/meta/0009_snapshot.json +8185 -0
- package/drizzle/meta/0010_snapshot.json +8813 -0
- package/drizzle/meta/_journal.json +71 -1
- package/package.json +3 -2
- package/dist/repositories/dashboard.d.ts +0 -14
- package/dist/repositories/dashboard.js +0 -55
- package/dist/repositories/dashboard.js.map +0 -1
- package/dist/repositories/learn.d.ts +0 -19
- package/dist/repositories/learn.js +0 -54
- package/dist/repositories/learn.js.map +0 -1
- package/dist/repositories/students.d.ts +0 -15
- package/dist/repositories/students.js +0 -83
- package/dist/repositories/students.js.map +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { entitlements } from "../schema/entitlements.js";
|
|
2
|
+
import { SQL, SQLWrapper } from "drizzle-orm";
|
|
3
|
+
//#region src/repositories/grant-covers.d.ts
|
|
4
|
+
type GrantedContentType = "course" | "download" | "community";
|
|
5
|
+
interface GrantColumns {
|
|
6
|
+
orgId: SQLWrapper;
|
|
7
|
+
contentId: SQLWrapper;
|
|
8
|
+
bundleId: SQLWrapper;
|
|
9
|
+
}
|
|
10
|
+
declare function grantUnexpired(expiresAt?: SQLWrapper, at?: SQL): SQL;
|
|
11
|
+
declare function grantExpired(expiresAt?: SQLWrapper, at?: SQL): SQL;
|
|
12
|
+
declare function grantIsActive(row?: typeof entitlements, at?: SQL): SQL;
|
|
13
|
+
declare function grantOpens(row: GrantColumns, contentId: SQL, type: GrantedContentType): SQL;
|
|
14
|
+
declare function activeGrantCovers(orgId: SQL, orgUserId: SQL, contentId: SQL, type: GrantedContentType): SQL;
|
|
15
|
+
declare function communityPublished(orgId: SQL, communityId: SQL): SQL;
|
|
16
|
+
declare function holdsCommunity(orgId: SQL, orgUserId: SQL, communityId: SQL): SQL;
|
|
17
|
+
declare function communityMember(orgId: SQL, orgUserId: SQL, role: SQL, communityId: SQL): SQL;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { GrantColumns, GrantedContentType, activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity };
|
|
20
|
+
//# sourceMappingURL=grant-covers.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { entitlements } from "../schema/entitlements.js";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { STAFF_ROLES } from "@headless-lms/core/organizations";
|
|
4
|
+
//#region src/repositories/grant-covers.ts
|
|
5
|
+
function grantUnexpired(expiresAt = entitlements.expiresAt, at = sql`now()`) {
|
|
6
|
+
return sql`(${expiresAt} is null or ${expiresAt} > ${at})`;
|
|
7
|
+
}
|
|
8
|
+
function grantExpired(expiresAt = entitlements.expiresAt, at = sql`now()`) {
|
|
9
|
+
return sql`(${expiresAt} is not null and ${expiresAt} <= ${at})`;
|
|
10
|
+
}
|
|
11
|
+
function grantIsActive(row = entitlements, at) {
|
|
12
|
+
return sql`(${row.status} = 'active' and ${grantUnexpired(row.expiresAt, at)})`;
|
|
13
|
+
}
|
|
14
|
+
function grantOpens(row, contentId, type) {
|
|
15
|
+
return sql`(
|
|
16
|
+
exists (
|
|
17
|
+
select 1 from content_items ci
|
|
18
|
+
where ci.org_id = ${row.orgId} and ci.id = ${contentId} and ci.type = ${type}
|
|
19
|
+
)
|
|
20
|
+
and (
|
|
21
|
+
${row.contentId} = ${contentId}
|
|
22
|
+
or exists (
|
|
23
|
+
select 1 from bundle_items bi
|
|
24
|
+
where bi.org_id = ${row.orgId}
|
|
25
|
+
and bi.bundle_id = ${row.bundleId}
|
|
26
|
+
and bi.content_id = ${contentId}
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
)`;
|
|
30
|
+
}
|
|
31
|
+
function activeGrantCovers(orgId, orgUserId, contentId, type) {
|
|
32
|
+
return sql`exists (
|
|
33
|
+
select 1 from content_access ca
|
|
34
|
+
join content_items ci on ci.org_id = ca.org_id and ci.id = ca.content_id and ci.type = ${type}
|
|
35
|
+
where ca.org_id = ${orgId}
|
|
36
|
+
and ca.org_user_id = ${orgUserId}
|
|
37
|
+
and ca.content_id = ${contentId}
|
|
38
|
+
)`;
|
|
39
|
+
}
|
|
40
|
+
function communityPublished(orgId, communityId) {
|
|
41
|
+
return sql`exists (
|
|
42
|
+
select 1 from communities pc
|
|
43
|
+
where pc.org_id = ${orgId} and pc.id = ${communityId} and pc.status = 'published'
|
|
44
|
+
)`;
|
|
45
|
+
}
|
|
46
|
+
function holdsCommunity(orgId, orgUserId, communityId) {
|
|
47
|
+
return sql`(${communityPublished(orgId, communityId)} and ${activeGrantCovers(orgId, orgUserId, communityId, "community")})`;
|
|
48
|
+
}
|
|
49
|
+
function communityMember(orgId, orgUserId, role, communityId) {
|
|
50
|
+
return sql`(${role} in (${sql.join(STAFF_ROLES.map((r) => sql`${r}`), sql`, `)}) or ${holdsCommunity(orgId, orgUserId, communityId)})`;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { activeGrantCovers, communityMember, communityPublished, grantExpired, grantIsActive, grantOpens, grantUnexpired, holdsCommunity };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=grant-covers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-covers.js","names":[],"sources":["../../src/repositories/grant-covers.ts"],"sourcesContent":["import { sql, type SQL, type SQLWrapper } from \"drizzle-orm\";\nimport { STAFF_ROLES } from \"@headless-lms/core/organizations\";\nimport { entitlements } from \"../schema/entitlements.js\";\n\nexport type GrantedContentType = \"course\" | \"download\" | \"community\";\n\nexport interface GrantColumns {\n orgId: SQLWrapper;\n contentId: SQLWrapper;\n bundleId: SQLWrapper;\n}\n\nexport function grantUnexpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is null or ${expiresAt} > ${at})`;\n}\n\nexport function grantExpired(\n expiresAt: SQLWrapper = entitlements.expiresAt,\n at: SQL = sql`now()`,\n): SQL {\n return sql`(${expiresAt} is not null and ${expiresAt} <= ${at})`;\n}\n\nexport function grantIsActive(row: typeof entitlements = entitlements, at?: SQL): SQL {\n return sql`(${row.status} = 'active' and ${grantUnexpired(row.expiresAt, at)})`;\n}\n\nexport function grantOpens(row: GrantColumns, contentId: SQL, type: GrantedContentType): SQL {\n return sql`(\n exists (\n select 1 from content_items ci\n where ci.org_id = ${row.orgId} and ci.id = ${contentId} and ci.type = ${type}\n )\n and (\n ${row.contentId} = ${contentId}\n or exists (\n select 1 from bundle_items bi\n where bi.org_id = ${row.orgId}\n and bi.bundle_id = ${row.bundleId}\n and bi.content_id = ${contentId}\n )\n )\n )`;\n}\n\nexport function activeGrantCovers(\n orgId: SQL,\n orgUserId: SQL,\n contentId: SQL,\n type: GrantedContentType,\n): SQL {\n return sql`exists (\n select 1 from content_access ca\n join content_items ci on ci.org_id = ca.org_id and ci.id = ca.content_id and ci.type = ${type}\n where ca.org_id = ${orgId}\n and ca.org_user_id = ${orgUserId}\n and ca.content_id = ${contentId}\n )`;\n}\n\nexport function communityPublished(orgId: SQL, communityId: SQL): SQL {\n return sql`exists (\n select 1 from communities pc\n where pc.org_id = ${orgId} and pc.id = ${communityId} and pc.status = 'published'\n )`;\n}\n\nexport function holdsCommunity(orgId: SQL, orgUserId: SQL, communityId: SQL): SQL {\n return sql`(${communityPublished(orgId, communityId)} and ${activeGrantCovers(orgId, orgUserId, communityId, \"community\")})`;\n}\n\nexport function communityMember(orgId: SQL, orgUserId: SQL, role: SQL, communityId: SQL): SQL {\n const staff = sql.join(\n STAFF_ROLES.map((r) => sql`${r}`),\n sql`, `,\n );\n return sql`(${role} in (${staff}) or ${holdsCommunity(orgId, orgUserId, communityId)})`;\n}\n"],"mappings":";;;;AAYA,SAAgB,eACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,cAAc,UAAU,KAAK,GAAG;AAC1D;AAEA,SAAgB,aACd,YAAwB,aAAa,WACrC,KAAU,GAAG,SACR;CACL,OAAO,GAAG,IAAI,UAAU,mBAAmB,UAAU,MAAM,GAAG;AAChE;AAEA,SAAgB,cAAc,MAA2B,cAAc,IAAe;CACpF,OAAO,GAAG,IAAI,IAAI,OAAO,kBAAkB,eAAe,IAAI,WAAW,EAAE,EAAE;AAC/E;AAEA,SAAgB,WAAW,KAAmB,WAAgB,MAA+B;CAC3F,OAAO,GAAG;;;0BAGc,IAAI,MAAM,eAAe,UAAU,iBAAiB,KAAK;;;QAG3E,IAAI,UAAU,KAAK,UAAU;;;4BAGT,IAAI,MAAM;+BACP,IAAI,SAAS;gCACZ,UAAU;;;;AAI1C;AAEA,SAAgB,kBACd,OACA,WACA,WACA,MACK;CACL,OAAO,GAAG;;6FAEiF,KAAK;wBAC1E,MAAM;6BACD,UAAU;4BACX,UAAU;;AAEtC;AAEA,SAAgB,mBAAmB,OAAY,aAAuB;CACpE,OAAO,GAAG;;wBAEY,MAAM,eAAe,YAAY;;AAEzD;AAEA,SAAgB,eAAe,OAAY,WAAgB,aAAuB;CAChF,OAAO,GAAG,IAAI,mBAAmB,OAAO,WAAW,EAAE,OAAO,kBAAkB,OAAO,WAAW,aAAa,WAAW,EAAE;AAC5H;AAEA,SAAgB,gBAAgB,OAAY,WAAgB,MAAW,aAAuB;CAK5F,OAAO,GAAG,IAAI,KAAK,OAJL,IAAI,KAChB,YAAY,KAAK,MAAM,GAAG,GAAG,GAAG,GAChC,GAAG,IAEyB,EAAE,OAAO,eAAe,OAAO,WAAW,WAAW,EAAE;AACvF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
-
import { CreateUserInput, Logger, UpdateUserInput, User } from "@headless-lms/core/types";
|
|
3
|
-
import { IdentityRepository } from "@headless-lms/core/identity";
|
|
2
|
+
import { CreateUserInput, Logger, UpdateUserInput, User, UserProfile } from "@headless-lms/core/types";
|
|
3
|
+
import { IdentityRepository, ProfilePatch } from "@headless-lms/core/identity";
|
|
4
4
|
//#region src/repositories/identity.d.ts
|
|
5
5
|
declare class DrizzleIdentityRepository implements IdentityRepository {
|
|
6
6
|
private readonly db;
|
|
@@ -11,6 +11,8 @@ declare class DrizzleIdentityRepository implements IdentityRepository {
|
|
|
11
11
|
findUserById(id: string): Promise<User | null>;
|
|
12
12
|
findUserByExternalId(externalId: string): Promise<User | null>;
|
|
13
13
|
findUserByEmail(email: string): Promise<User | null>;
|
|
14
|
+
findProfile(userId: string): Promise<UserProfile | null>;
|
|
15
|
+
upsertProfile(userId: string, patch: ProfilePatch): Promise<UserProfile>;
|
|
14
16
|
}
|
|
15
17
|
//#endregion
|
|
16
18
|
export { DrizzleIdentityRepository };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { users } from "../schema/identity.js";
|
|
1
|
+
import { userProfiles, users } from "../schema/identity.js";
|
|
2
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
2
3
|
import { eq, sql } from "drizzle-orm";
|
|
3
4
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/repositories/identity.ts
|
|
@@ -37,7 +38,39 @@ var DrizzleIdentityRepository = class {
|
|
|
37
38
|
const [row] = await this.db.select().from(users).where(sql`lower(${users.email}) = lower(${email})`).limit(1);
|
|
38
39
|
return row ?? null;
|
|
39
40
|
}
|
|
41
|
+
async findProfile(userId) {
|
|
42
|
+
const [row] = await this.db.select().from(userProfiles).where(eq(userProfiles.userId, userId)).limit(1);
|
|
43
|
+
return row ?? null;
|
|
44
|
+
}
|
|
45
|
+
async upsertProfile(userId, patch) {
|
|
46
|
+
if (!await this.findProfile(userId)) {
|
|
47
|
+
const [row] = await this.db.insert(userProfiles).values({
|
|
48
|
+
userId,
|
|
49
|
+
displayName: patch.displayName ?? null,
|
|
50
|
+
headline: patch.headline ?? null,
|
|
51
|
+
bio: patch.bio ?? null,
|
|
52
|
+
imageAssetOrgId: patch.imageAssetOrgId ?? null,
|
|
53
|
+
imageAssetId: patch.imageAssetId ?? null,
|
|
54
|
+
extra: patch.extra ?? {}
|
|
55
|
+
}).returning();
|
|
56
|
+
if (!row) throw new Error("failed to insert profile");
|
|
57
|
+
return row;
|
|
58
|
+
}
|
|
59
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
60
|
+
if (patch.displayName !== void 0) set.displayName = patch.displayName;
|
|
61
|
+
if (patch.headline !== void 0) set.headline = patch.headline;
|
|
62
|
+
if (patch.bio !== void 0) set.bio = patch.bio;
|
|
63
|
+
if (patch.imageAssetOrgId !== void 0) {
|
|
64
|
+
set.imageAssetOrgId = patch.imageAssetOrgId;
|
|
65
|
+
set.imageAssetId = patch.imageAssetId;
|
|
66
|
+
}
|
|
67
|
+
if (patch.extra !== void 0) set.extra = patch.extra;
|
|
68
|
+
const [row] = await this.db.update(userProfiles).set(set).where(eq(userProfiles.userId, userId)).returning();
|
|
69
|
+
if (!row) throw new Error("failed to update profile");
|
|
70
|
+
return row;
|
|
71
|
+
}
|
|
40
72
|
};
|
|
73
|
+
translateDbErrors(DrizzleIdentityRepository);
|
|
41
74
|
//#endregion
|
|
42
75
|
export { DrizzleIdentityRepository };
|
|
43
76
|
|
|
@@ -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
|
|
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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"}
|
|
@@ -10,7 +10,7 @@ declare class DrizzleConnectionsRepository implements ConnectionsRepository {
|
|
|
10
10
|
findById(orgId: string, id: string): Promise<Connection | null>;
|
|
11
11
|
findByIntegration(orgId: string, integrationId: string): Promise<Connection | null>;
|
|
12
12
|
list(orgId: string): Promise<Connection[]>;
|
|
13
|
-
update(orgId: string, id: string, patch: Partial<Pick<Connection,
|
|
13
|
+
update(orgId: string, id: string, patch: Partial<Pick<Connection, "config" | "active" | "updatedAt">>): Promise<Connection | null>;
|
|
14
14
|
delete(orgId: string, id: string): Promise<boolean>;
|
|
15
15
|
}
|
|
16
16
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connections } from "../schema/integrations.js";
|
|
2
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
2
3
|
import { and, asc, eq } from "drizzle-orm";
|
|
3
4
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
5
|
//#region src/repositories/integrations.ts
|
|
@@ -59,6 +60,7 @@ var DrizzleConnectionsRepository = class {
|
|
|
59
60
|
return (await this.db.delete(connections).where(and(eq(connections.orgId, orgId), eq(connections.id, id))).returning({ id: connections.id })).length > 0;
|
|
60
61
|
}
|
|
61
62
|
};
|
|
63
|
+
translateDbErrors(DrizzleConnectionsRepository);
|
|
62
64
|
//#endregion
|
|
63
65
|
export { DrizzleConnectionsRepository };
|
|
64
66
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integrations.js","names":[],"sources":["../../src/repositories/integrations.ts"],"sourcesContent":["// integrations — Drizzle repository (implements the core outbound port).\nimport { and, asc, eq } from
|
|
1
|
+
{"version":3,"file":"integrations.js","names":[],"sources":["../../src/repositories/integrations.ts"],"sourcesContent":["// integrations — Drizzle repository (implements the core outbound port).\nimport { and, asc, eq } from \"drizzle-orm\";\nimport type { DbExecutor } from \"../client.js\";\nimport type { Connection, ConnectionsRepository } from \"@headless-lms/core/integrations\";\nimport { connections } from \"../schema/integrations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof connections.$inferSelect;\n\nfunction toConnection(row: Row): Connection {\n return {\n orgId: row.orgId,\n id: row.id,\n integrationId: row.integrationId,\n config: row.config,\n active: row.active,\n credentialRef: row.credentialRef,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleConnectionsRepository implements ConnectionsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, connection: Connection): Promise<Connection> {\n if (connection.orgId !== orgId) {\n throw new Error(\"connection org mismatch\");\n }\n const [row] = await this.db\n .insert(connections)\n .values({\n orgId: connection.orgId,\n id: connection.id,\n integrationId: connection.integrationId,\n config: connection.config,\n active: connection.active,\n credentialRef: connection.credentialRef,\n createdAt: new Date(connection.createdAt),\n updatedAt: new Date(connection.updatedAt),\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert connection\");\n }\n return toConnection(row);\n }\n\n async findById(orgId: string, id: string): Promise<Connection | null> {\n const [row] = await this.db\n .select()\n .from(connections)\n .where(and(eq(connections.orgId, orgId), eq(connections.id, id)))\n .limit(1);\n return row ? toConnection(row) : null;\n }\n\n async findByIntegration(orgId: string, integrationId: string): Promise<Connection | null> {\n const [row] = await this.db\n .select()\n .from(connections)\n .where(and(eq(connections.orgId, orgId), eq(connections.integrationId, integrationId)))\n .limit(1);\n return row ? toConnection(row) : null;\n }\n\n async list(orgId: string): Promise<Connection[]> {\n const rows = await this.db\n .select()\n .from(connections)\n .where(eq(connections.orgId, orgId))\n .orderBy(asc(connections.integrationId));\n return rows.map(toConnection);\n }\n\n async update(\n orgId: string,\n id: string,\n patch: Partial<Pick<Connection, \"config\" | \"active\" | \"updatedAt\">>,\n ): Promise<Connection | null> {\n const [row] = await this.db\n .update(connections)\n .set({\n ...(patch.config !== undefined ? { config: patch.config } : {}),\n ...(patch.active !== undefined ? { active: patch.active } : {}),\n ...(patch.updatedAt !== undefined ? { updatedAt: new Date(patch.updatedAt) } : {}),\n })\n .where(and(eq(connections.orgId, orgId), eq(connections.id, id)))\n .returning();\n return row ? toConnection(row) : null;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n const deleted = await this.db\n .delete(connections)\n .where(and(eq(connections.orgId, orgId), eq(connections.id, id)))\n .returning({ id: connections.id });\n return deleted.length > 0;\n }\n}\ntranslateDbErrors(DrizzleConnectionsRepository);\n"],"mappings":";;;;;AAWA,SAAS,aAAa,KAAsB;CAC1C,OAAO;EACL,OAAO,IAAI;EACX,IAAI,IAAI;EACR,eAAe,IAAI;EACnB,QAAQ,IAAI;EACZ,QAAQ,IAAI;EACZ,eAAe,IAAI;EACnB,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,+BAAb,MAA2E;CAEtD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,YAA6C;EACvE,IAAI,WAAW,UAAU,OACvB,MAAM,IAAI,MAAM,yBAAyB;EAE3C,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,WAAW,CAAC,CACnB,OAAO;GACN,OAAO,WAAW;GAClB,IAAI,WAAW;GACf,eAAe,WAAW;GAC1B,QAAQ,WAAW;GACnB,QAAQ,WAAW;GACnB,eAAe,WAAW;GAC1B,WAAW,IAAI,KAAK,WAAW,SAAS;GACxC,WAAW,IAAI,KAAK,WAAW,SAAS;EAC1C,CAAC,CAAC,CACD,UAAU;EACb,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,6BAA6B;EAE/C,OAAO,aAAa,GAAG;CACzB;CAEA,MAAM,SAAS,OAAe,IAAwC;EACpE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,MAAM,CAAC;EACV,OAAO,MAAM,aAAa,GAAG,IAAI;CACnC;CAEA,MAAM,kBAAkB,OAAe,eAAmD;EACxF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,eAAe,aAAa,CAAC,CAAC,CAAC,CACtF,MAAM,CAAC;EACV,OAAO,MAAM,aAAa,GAAG,IAAI;CACnC;CAEA,MAAM,KAAK,OAAsC;EAM/C,QAAO,MALY,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MAAM,GAAG,YAAY,OAAO,KAAK,CAAC,CAAC,CACnC,QAAQ,IAAI,YAAY,aAAa,CAAC,EAAA,CAC7B,IAAI,YAAY;CAC9B;CAEA,MAAM,OACJ,OACA,IACA,OAC4B;EAC5B,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,WAAW,CAAC,CACnB,IAAI;GACH,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;GAC7D,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;GAC7D,GAAI,MAAM,cAAc,KAAA,IAAY,EAAE,WAAW,IAAI,KAAK,MAAM,SAAS,EAAE,IAAI,CAAC;EAClF,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,UAAU;EACb,OAAO,MAAM,aAAa,GAAG,IAAI;CACnC;CAEA,MAAM,OAAO,OAAe,IAA8B;EAKxD,QAAO,MAJe,KAAK,GACxB,OAAO,WAAW,CAAC,CACnB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,UAAU,EAAE,IAAI,YAAY,GAAG,CAAC,EAAA,CACpB,SAAS;CAC1B;AACF;AACA,kBAAkB,4BAA4B"}
|
|
@@ -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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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { users } from "../schema/identity.js";
|
|
2
2
|
import { invites, orgUsers } from "../schema/organizations.js";
|
|
3
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
3
4
|
import { user } from "../schema/better-auth.js";
|
|
4
5
|
import { orgUserProfileColumns } from "./org-user-profile.js";
|
|
5
6
|
import { and, eq, ne } from "drizzle-orm";
|
|
6
7
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
7
|
-
import { STUDENT_ROLE,
|
|
8
|
+
import { STUDENT_ROLE, isRole } from "@headless-lms/core/organizations";
|
|
8
9
|
//#region src/repositories/members.ts
|
|
9
|
-
const roleOf = (t) =>
|
|
10
|
+
const roleOf = (t) => isRole(t) && t !== STUDENT_ROLE ? t : "instructor";
|
|
10
11
|
function toMember(r) {
|
|
11
12
|
return {
|
|
12
13
|
id: r.id,
|
|
@@ -110,6 +111,7 @@ var DrizzleMembersRepository = class {
|
|
|
110
111
|
return (await this.loadAll(orgId)).find((r) => r.id === id) ?? null;
|
|
111
112
|
}
|
|
112
113
|
};
|
|
114
|
+
translateDbErrors(DrizzleMembersRepository);
|
|
113
115
|
//#endregion
|
|
114
116
|
export { DrizzleMembersRepository };
|
|
115
117
|
|
|
@@ -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
|
|
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"}
|