@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
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @headless-lms/adapter-db
|
|
2
|
+
|
|
3
|
+
Drizzle/Postgres persistence adapter. Implements the outbound repository ports from
|
|
4
|
+
`@headless-lms/core` and ships the migrations for the LMS database.
|
|
5
|
+
|
|
6
|
+
It depends on `@headless-lms/core`, `drizzle-orm` and `pg` — never on the server or
|
|
7
|
+
any other adapter.
|
|
8
|
+
|
|
9
|
+
## Layout
|
|
10
|
+
|
|
11
|
+
| Path | What lives there |
|
|
12
|
+
| --------------------- | -------------------------------------------------------------------------- |
|
|
13
|
+
| `src/schema/` | Drizzle tables, grouped by owning context, barrelled in `index.ts` |
|
|
14
|
+
| `src/repositories/` | One `Drizzle*Repository` per context port, plus pg error translation |
|
|
15
|
+
| `src/client.ts` | `createDb` — pool + drizzle instance; `Db`, `Tx`, `DbExecutor` types |
|
|
16
|
+
| `src/unit-of-work.ts` | `DrizzleUnitOfWork` — transaction scope for the core `UnitOfWork` port |
|
|
17
|
+
| `src/migrate.ts` | `runMigrations(databaseUrl)` against the packaged `drizzle/` folder |
|
|
18
|
+
| `drizzle/` | Generated SQL migrations and snapshots (committed, shipped in the package) |
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createDb, runMigrations, DrizzleProgressRepository } from "@headless-lms/adapter-db";
|
|
24
|
+
|
|
25
|
+
await runMigrations(process.env.DATABASE_URL!);
|
|
26
|
+
|
|
27
|
+
const db = createDb(process.env.DATABASE_URL!);
|
|
28
|
+
const progress = new DrizzleProgressRepository(db, logger);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Repositories take a `DbExecutor`, so the same class works against the root `db` or a
|
|
32
|
+
transaction handle. `DrizzleUnitOfWork` builds a tx-bound scope, guaranteeing nothing
|
|
33
|
+
escapes the transaction:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const uow = new DrizzleUnitOfWork(db, (tx) => ({
|
|
37
|
+
progress: new DrizzleProgressRepository(tx),
|
|
38
|
+
outbox: new DrizzleOutboxRepository(tx),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
await uow.run(async ({ progress, outbox }) => {
|
|
42
|
+
/* ... */
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Error translation
|
|
47
|
+
|
|
48
|
+
`repositories/pg-errors.ts` turns driver failures into domain errors — unique violations
|
|
49
|
+
become `ConflictError`, everything else becomes a `DbQueryError` carrying the SQL text and
|
|
50
|
+
structured pg fields but never the bind params (they can hold PII).
|
|
51
|
+
|
|
52
|
+
## Drift checks
|
|
53
|
+
|
|
54
|
+
`schema/drift.ts` provides compile-time assertions that a table's row type matches its
|
|
55
|
+
domain type from `@headless-lms/core/schemas`. A row field may be wider than the domain's,
|
|
56
|
+
never narrower, missing, or extra. Drift fails `pnpm typecheck` with the offending keys.
|
|
57
|
+
|
|
58
|
+
## Migration workflow
|
|
59
|
+
|
|
60
|
+
Push during dev, generate once at the end.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm db:push # apply schema changes straight to the dev database, no files
|
|
64
|
+
pnpm db:generate # diff against the last committed snapshot → one migration
|
|
65
|
+
pnpm db:migrate # apply pending migrations
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
While iterating locally, use `db:push`. When the schema is settled and ready to ship, run
|
|
69
|
+
`db:generate` once — it diffs against the last committed snapshot, so you get a single
|
|
70
|
+
migration containing everything since the previous release.
|
|
71
|
+
|
|
72
|
+
All three read `DATABASE_URL` from the repo-root `.env`.
|
|
73
|
+
|
|
74
|
+
## Tests
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm test # vitest
|
|
78
|
+
pnpm typecheck
|
|
79
|
+
```
|
package/dist/client.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
3
3
|
//#region src/client.d.ts
|
|
4
4
|
/** Transaction executor — the same query surface as the root db.
|
|
5
5
|
* (Promoted from the structure.ts repository's local alias.) */
|
|
6
|
-
type Tx = Parameters<Parameters<NodePgDatabase[
|
|
6
|
+
type Tx = Parameters<Parameters<NodePgDatabase["transaction"]>[0]>[0];
|
|
7
7
|
/** What a Drizzle repository executes against: the root db OR a transaction
|
|
8
8
|
* handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no
|
|
9
9
|
* statement escapes the transaction. */
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Drizzle client + connection. Owns connection pooling (Drizzle does not).\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport pg from \"pg\";\n\n/** Transaction executor — the same query surface as the root db.\n * (Promoted from the structure.ts repository's local alias.) */\nexport type Tx = Parameters<Parameters<NodePgDatabase[\"transaction\"]>[0]>[0];\n\n/** What a Drizzle repository executes against: the root db OR a transaction\n * handle — tx-bound repo instances are how DrizzleUnitOfWork guarantees no\n * statement escapes the transaction. */\nexport type DbExecutor = NodePgDatabase | Tx;\n\n// Centralized schema — re-exported so the db adapter is the single source for tables.\nexport * as schema from \"./schema/index.js\";\n\nexport type Db = ReturnType<typeof createDb>;\n\nexport function createDb(connectionString: string) {\n const pool = new pg.Pool({ connectionString });\n return drizzle(pool);\n}\n"],"mappings":";;;;AAkBA,SAAgB,SAAS,kBAA0B;CAEjD,OAAO,QAAQ,IADE,GAAG,KAAK,EAAE,iBAAiB,CAC1B,CAAC;AACrB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,17 +6,19 @@ import { DrizzleAssetsRepository } from "./repositories/assets.js";
|
|
|
6
6
|
import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
|
|
7
7
|
import { DrizzleContentRepository } from "./repositories/content.js";
|
|
8
8
|
import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
|
|
9
|
-
import { DrizzleDashboardRepository } from "./repositories/dashboard.js";
|
|
10
9
|
import { DrizzleDiscussionRepository } from "./repositories/discussion.js";
|
|
10
|
+
import { DrizzlePostRepository } from "./repositories/posts.js";
|
|
11
|
+
import { DrizzleChatRepository } from "./repositories/chats.js";
|
|
11
12
|
import { DrizzleEntitlementsRepository } from "./repositories/entitlements.js";
|
|
12
13
|
import { DrizzleIdentityRepository } from "./repositories/identity.js";
|
|
13
14
|
import { DrizzleConnectionsRepository } from "./repositories/integrations.js";
|
|
14
|
-
import {
|
|
15
|
+
import { DrizzleMemberRepository } from "./repositories/member.js";
|
|
15
16
|
import { DrizzleMembersRepository } from "./repositories/members.js";
|
|
16
|
-
import {
|
|
17
|
+
import { DrizzleNotificationsRepository } from "./repositories/notifications.js";
|
|
18
|
+
import { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./repositories/org-user-profile.js";
|
|
17
19
|
import { DrizzleOrganizationsRepository } from "./repositories/organizations.js";
|
|
18
20
|
import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS } from "./repositories/outbox.js";
|
|
19
21
|
import { DrizzleProgressRepository } from "./repositories/progress.js";
|
|
20
22
|
import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
|
|
21
|
-
import {
|
|
22
|
-
export { type Db, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore,
|
|
23
|
+
import { DrizzleAdminRepository } from "./repositories/admin.js";
|
|
24
|
+
export { type Db, DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, index_d_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
package/dist/index.js
CHANGED
|
@@ -4,19 +4,21 @@ import { DrizzleUnitOfWork } from "./unit-of-work.js";
|
|
|
4
4
|
import { migrationsFolder, runMigrations } from "./migrate.js";
|
|
5
5
|
import { DrizzleAssetsRepository } from "./repositories/assets.js";
|
|
6
6
|
import { DrizzleAutomationRunsRepository, DrizzleAutomationsRepository } from "./repositories/automations.js";
|
|
7
|
+
import { DrizzleSettingsRepository, deepMerge } from "./repositories/settings.js";
|
|
7
8
|
import { DrizzleContentRepository } from "./repositories/content.js";
|
|
8
9
|
import { DrizzleCredentialStore, decryptSecret, encryptSecret } from "./repositories/credentials.js";
|
|
9
|
-
import {
|
|
10
|
-
import { orgUserProfileColumns } from "./repositories/org-user-profile.js";
|
|
10
|
+
import { authorProfileColumns, orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./repositories/org-user-profile.js";
|
|
11
11
|
import { DrizzleDiscussionRepository } from "./repositories/discussion.js";
|
|
12
|
+
import { DrizzlePostRepository } from "./repositories/posts.js";
|
|
13
|
+
import { DrizzleChatRepository } from "./repositories/chats.js";
|
|
12
14
|
import { DrizzleEntitlementsRepository } from "./repositories/entitlements.js";
|
|
13
15
|
import { DrizzleIdentityRepository } from "./repositories/identity.js";
|
|
14
16
|
import { DrizzleConnectionsRepository } from "./repositories/integrations.js";
|
|
15
|
-
import {
|
|
17
|
+
import { DrizzleMemberRepository } from "./repositories/member.js";
|
|
16
18
|
import { DrizzleMembersRepository } from "./repositories/members.js";
|
|
19
|
+
import { DrizzleNotificationsRepository } from "./repositories/notifications.js";
|
|
17
20
|
import { DrizzleOrganizationsRepository } from "./repositories/organizations.js";
|
|
18
21
|
import { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS } from "./repositories/outbox.js";
|
|
19
22
|
import { DrizzleProgressRepository } from "./repositories/progress.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
export { DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDashboardRepository, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleLearnRepository, DrizzleMembersRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleStudentsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema };
|
|
23
|
+
import { DrizzleAdminRepository } from "./repositories/admin.js";
|
|
24
|
+
export { DrizzleAdminRepository, DrizzleAssetsRepository, DrizzleAutomationRunsRepository, DrizzleAutomationsRepository, DrizzleChatRepository, DrizzleConnectionsRepository, DrizzleContentRepository, DrizzleCredentialStore, DrizzleDiscussionRepository, DrizzleEntitlementsRepository, DrizzleIdentityRepository, DrizzleMemberRepository, DrizzleMembersRepository, DrizzleNotificationsRepository, DrizzleOrganizationsRepository, DrizzleOutboxAppender, DrizzleOutboxStore, DrizzlePostRepository, DrizzleProgressRepository, DrizzleSettingsRepository, DrizzleUnitOfWork, OUTBOX_MAX_ATTEMPTS, authorProfileColumns, createDb, decryptSecret, deepMerge, encryptSecret, migrationsFolder, orgUserProfileColumns, runMigrations, schema_exports as schema, studentFirstName, studentLastName, studentProfileColumns };
|
package/dist/migrate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.js","names":[],"sources":["../src/migrate.ts"],"sourcesContent":["// Runs the drizzle migrations shipped inside this package against the\n// installation's database. Fails loudly and legibly — this is the first\n// command every new installation runs.\nimport { fileURLToPath } from
|
|
1
|
+
{"version":3,"file":"migrate.js","names":[],"sources":["../src/migrate.ts"],"sourcesContent":["// Runs the drizzle migrations shipped inside this package against the\n// installation's database. Fails loudly and legibly — this is the first\n// command every new installation runs.\nimport { fileURLToPath } from \"node:url\";\nimport pg from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { migrate } from \"drizzle-orm/node-postgres/migrator\";\n\n/** drizzle/ sits at the package root: ../drizzle from src/ AND dist/. */\nexport function migrationsFolder(): string {\n return fileURLToPath(new URL(\"../drizzle/\", import.meta.url));\n}\n\nexport async function runMigrations(databaseUrl: string): Promise<void> {\n if (!databaseUrl) {\n throw new Error(\n \"DATABASE_URL is not set. Put it in your .env (e.g. postgres://postgres:postgres@localhost:8005/headless_lms) and re-run.\",\n );\n }\n const pool = new pg.Pool({ connectionString: databaseUrl, max: 1 });\n try {\n await migrate(drizzle(pool), { migrationsFolder: migrationsFolder() });\n } catch (err) {\n throw new Error(\n `Migration failed against ${databaseUrl.replace(/\\/\\/.*@/, \"//***@\")}: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n );\n } finally {\n await pool.end();\n }\n}\n"],"mappings":";;;;;;AASA,SAAgB,mBAA2B;CACzC,OAAO,cAAc,IAAI,IAAI,eAAe,OAAO,KAAK,GAAG,CAAC;AAC9D;AAEA,eAAsB,cAAc,aAAoC;CACtE,IAAI,CAAC,aACH,MAAM,IAAI,MACR,0HACF;CAEF,MAAM,OAAO,IAAI,GAAG,KAAK;EAAE,kBAAkB;EAAa,KAAK;CAAE,CAAC;CAClE,IAAI;EACF,MAAM,QAAQ,QAAQ,IAAI,GAAG,EAAE,kBAAkB,iBAAiB,EAAE,CAAC;CACvE,SAAS,KAAK;EACZ,MAAM,IAAI,MACR,4BAA4B,YAAY,QAAQ,WAAW,QAAQ,EAAE,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,KACxH,EAAE,OAAO,IAAI,CACf;CACF,UAAU;EACR,MAAM,KAAK,IAAI;CACjB;AACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
|
+
import { AdminReportRepository, CommunityAnalytics, CommunityMember, CommunityMembersQuery, CourseAnalytics, CourseEnrollmentPoint, EnrollmentPoint, OverviewStats, Page, Student, StudentAnalytics, StudentsQuery } from "@headless-lms/core/reporting/admin";
|
|
4
|
+
//#region src/repositories/admin.d.ts
|
|
5
|
+
declare class DrizzleAdminRepository implements AdminReportRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
|
+
overview(orgId: string): Promise<OverviewStats>;
|
|
10
|
+
enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]>;
|
|
11
|
+
private courseExists;
|
|
12
|
+
courseAnalytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
13
|
+
courseEnrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
14
|
+
listStudents(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
|
|
15
|
+
getStudent(orgId: string, id: string): Promise<Student | null>;
|
|
16
|
+
studentAnalytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
17
|
+
private resolveOrder;
|
|
18
|
+
communityAnalytics(orgId: string, communityId: string): Promise<CommunityAnalytics | null>;
|
|
19
|
+
communityMembers(orgId: string, communityId: string, query: CommunityMembersQuery): Promise<Page<CommunityMember> | null>;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { DrizzleAdminRepository };
|
|
23
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { communities, courses, spaces } from "../schema/content.js";
|
|
2
|
+
import { commentFlags, comments, messageFlags, messages, postFlags, posts } from "../schema/discussion.js";
|
|
3
|
+
import { users } from "../schema/identity.js";
|
|
4
|
+
import { orgUsers } from "../schema/organizations.js";
|
|
5
|
+
import { contentAccess, entitlements } from "../schema/entitlements.js";
|
|
6
|
+
import "../schema/index.js";
|
|
7
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
8
|
+
import { user } from "../schema/better-auth.js";
|
|
9
|
+
import { orgUserProfileColumns, studentFirstName, studentLastName, studentProfileColumns } from "./org-user-profile.js";
|
|
10
|
+
import { grantExpired, grantOpens } from "./grant-covers.js";
|
|
11
|
+
import { and, asc, desc, eq, exists, ilike, inArray, or, sql } from "drizzle-orm";
|
|
12
|
+
import { union } from "drizzle-orm/pg-core";
|
|
13
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
14
|
+
import { STUDENT_ROLE } from "@headless-lms/core/organizations";
|
|
15
|
+
//#region src/repositories/admin.ts
|
|
16
|
+
const entitlementCountExpr = sql`count(${entitlements.id})`;
|
|
17
|
+
const avgProgressExpr = sql`0`;
|
|
18
|
+
const lastActiveExpr = sql`(
|
|
19
|
+
select max(pr.updated_at) from progress_records pr
|
|
20
|
+
where pr.org_id = ${orgUsers.orgId} and pr.org_user_id = ${orgUsers.id}
|
|
21
|
+
)`;
|
|
22
|
+
function toStudent(row) {
|
|
23
|
+
return {
|
|
24
|
+
id: row.id,
|
|
25
|
+
email: row.email,
|
|
26
|
+
image: row.image ?? null,
|
|
27
|
+
firstName: row.firstName,
|
|
28
|
+
lastName: row.lastName,
|
|
29
|
+
status: row.status,
|
|
30
|
+
entitlementCount: Number(row.entitlementCount),
|
|
31
|
+
avgProgress: Number(row.avgProgress),
|
|
32
|
+
joinedAt: row.createdAt.toISOString(),
|
|
33
|
+
lastActiveAt: row.lastActiveAt ? new Date(row.lastActiveAt).toISOString() : null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var DrizzleAdminRepository = class {
|
|
37
|
+
db;
|
|
38
|
+
logger;
|
|
39
|
+
constructor(db, logger = noopLogger) {
|
|
40
|
+
this.db = db;
|
|
41
|
+
this.logger = logger;
|
|
42
|
+
}
|
|
43
|
+
async overview(orgId) {
|
|
44
|
+
const [courseCounts] = await this.db.select({
|
|
45
|
+
published: sql`count(*) filter (where ${courses.status} = 'published')`,
|
|
46
|
+
draft: sql`count(*) filter (where ${courses.status} = 'draft')`
|
|
47
|
+
}).from(courses).where(eq(courses.orgId, orgId));
|
|
48
|
+
const [entitlementCounts] = await this.db.select({
|
|
49
|
+
activeStudents: sql`count(distinct ${contentAccess.orgUserId})`,
|
|
50
|
+
expiringSoon: sql`count(distinct ${contentAccess.entitlementId}) filter (where ${grantExpired(contentAccess.expiresAt, sql`now() + interval '14 days'`)})`
|
|
51
|
+
}).from(contentAccess).where(eq(contentAccess.orgId, orgId));
|
|
52
|
+
return {
|
|
53
|
+
publishedCourses: Number(courseCounts?.published ?? 0),
|
|
54
|
+
draftCourses: Number(courseCounts?.draft ?? 0),
|
|
55
|
+
activeStudents: Number(entitlementCounts?.activeStudents ?? 0),
|
|
56
|
+
expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async enrollments(orgId, days) {
|
|
60
|
+
return (await this.db.execute(sql`
|
|
61
|
+
select
|
|
62
|
+
to_char(d.day, 'YYYY-MM-DD') as date,
|
|
63
|
+
count(${entitlements.id})::int as count
|
|
64
|
+
from generate_series(
|
|
65
|
+
(now() at time zone 'utc')::date - (${days} - 1),
|
|
66
|
+
(now() at time zone 'utc')::date,
|
|
67
|
+
interval '1 day'
|
|
68
|
+
) as d(day)
|
|
69
|
+
left join ${entitlements}
|
|
70
|
+
on ${entitlements.orgId} = ${orgId}
|
|
71
|
+
and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
|
|
72
|
+
group by d.day
|
|
73
|
+
order by d.day
|
|
74
|
+
`)).rows.map((r) => ({
|
|
75
|
+
date: r.date,
|
|
76
|
+
count: Number(r.count)
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
async courseExists(orgId, courseId) {
|
|
80
|
+
const [row] = await this.db.select({ id: courses.id }).from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));
|
|
81
|
+
return row !== void 0;
|
|
82
|
+
}
|
|
83
|
+
async courseAnalytics(orgId, courseId) {
|
|
84
|
+
if (!await this.courseExists(orgId, courseId)) return null;
|
|
85
|
+
const cohort = sql`
|
|
86
|
+
select distinct org_user_id from content_access
|
|
87
|
+
where org_id = ${orgId}
|
|
88
|
+
and content_id = ${courseId}
|
|
89
|
+
`;
|
|
90
|
+
const publishedActivities = sql`
|
|
91
|
+
select a.id, a.settings, a.seq, a.module_id
|
|
92
|
+
from activities a
|
|
93
|
+
where a.org_id = ${orgId}
|
|
94
|
+
and a.course_id = ${courseId}
|
|
95
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
96
|
+
`;
|
|
97
|
+
const summary = (await this.db.execute(sql`
|
|
98
|
+
with cohort as (${cohort}),
|
|
99
|
+
acts as (${publishedActivities}),
|
|
100
|
+
per_learner as (
|
|
101
|
+
select c.org_user_id,
|
|
102
|
+
count(pr.id) filter (where pr.completed_at is not null) as done
|
|
103
|
+
from cohort c
|
|
104
|
+
left join progress_records pr
|
|
105
|
+
on pr.org_id = ${orgId}
|
|
106
|
+
and pr.org_user_id = c.org_user_id
|
|
107
|
+
and pr.target_type = 'activity'
|
|
108
|
+
and pr.target_id in (select id from acts)
|
|
109
|
+
group by c.org_user_id
|
|
110
|
+
)
|
|
111
|
+
select
|
|
112
|
+
(select count(*) from cohort)::int as enrolled,
|
|
113
|
+
(
|
|
114
|
+
select count(distinct pr.org_user_id) from progress_records pr
|
|
115
|
+
where pr.org_id = ${orgId}
|
|
116
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
117
|
+
and (
|
|
118
|
+
(pr.target_type = 'activity' and pr.target_id in (select id from acts))
|
|
119
|
+
or (pr.target_type = 'course' and pr.target_id = ${courseId})
|
|
120
|
+
)
|
|
121
|
+
)::int as started,
|
|
122
|
+
(
|
|
123
|
+
select count(*) from progress_records pr
|
|
124
|
+
where pr.org_id = ${orgId}
|
|
125
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
126
|
+
and pr.target_type = 'course'
|
|
127
|
+
and pr.target_id = ${courseId}
|
|
128
|
+
and pr.completed_at is not null
|
|
129
|
+
)::int as completed,
|
|
130
|
+
coalesce((
|
|
131
|
+
select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))
|
|
132
|
+
from per_learner
|
|
133
|
+
), 0)::int as avg_progress
|
|
134
|
+
`)).rows[0];
|
|
135
|
+
const activities = (await this.db.execute(sql`
|
|
136
|
+
with cohort as (${cohort})
|
|
137
|
+
select
|
|
138
|
+
a.id,
|
|
139
|
+
coalesce(a.settings ->> 'title', '') as title,
|
|
140
|
+
m.id as module_id,
|
|
141
|
+
m.title as module_title,
|
|
142
|
+
count(pr.id)::int as started,
|
|
143
|
+
count(pr.id) filter (where pr.completed_at is not null)::int as completed
|
|
144
|
+
from activities a
|
|
145
|
+
join modules m on m.org_id = a.org_id and m.id = a.module_id
|
|
146
|
+
left join progress_records pr
|
|
147
|
+
on pr.org_id = ${orgId}
|
|
148
|
+
and pr.target_type = 'activity'
|
|
149
|
+
and pr.target_id = a.id
|
|
150
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
151
|
+
where a.org_id = ${orgId}
|
|
152
|
+
and a.course_id = ${courseId}
|
|
153
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
154
|
+
group by a.id, a.settings, m.id, m.title, m.seq, a.seq
|
|
155
|
+
order by m.seq, a.seq
|
|
156
|
+
`)).rows.map((r) => ({
|
|
157
|
+
activityId: r.id,
|
|
158
|
+
title: r.title,
|
|
159
|
+
moduleId: r.module_id,
|
|
160
|
+
moduleTitle: r.module_title,
|
|
161
|
+
started: Number(r.started),
|
|
162
|
+
completed: Number(r.completed)
|
|
163
|
+
}));
|
|
164
|
+
return {
|
|
165
|
+
enrolled: Number(summary?.enrolled ?? 0),
|
|
166
|
+
started: Number(summary?.started ?? 0),
|
|
167
|
+
completed: Number(summary?.completed ?? 0),
|
|
168
|
+
avgProgress: Number(summary?.avg_progress ?? 0),
|
|
169
|
+
activities
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async courseEnrollments(orgId, courseId, days) {
|
|
173
|
+
if (!await this.courseExists(orgId, courseId)) return null;
|
|
174
|
+
return (await this.db.execute(sql`
|
|
175
|
+
select
|
|
176
|
+
to_char(d.day, 'YYYY-MM-DD') as date,
|
|
177
|
+
count(${entitlements.id})::int as count
|
|
178
|
+
from generate_series(
|
|
179
|
+
(now() at time zone 'utc')::date - (${days} - 1),
|
|
180
|
+
(now() at time zone 'utc')::date,
|
|
181
|
+
interval '1 day'
|
|
182
|
+
) as d(day)
|
|
183
|
+
left join ${entitlements}
|
|
184
|
+
on ${entitlements.orgId} = ${orgId}
|
|
185
|
+
and ${entitlements.contentId} = ${courseId}
|
|
186
|
+
and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
|
|
187
|
+
group by d.day
|
|
188
|
+
order by d.day
|
|
189
|
+
`)).rows.map((r) => ({
|
|
190
|
+
date: r.date,
|
|
191
|
+
count: Number(r.count)
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
async listStudents(orgId, query) {
|
|
195
|
+
const filters = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];
|
|
196
|
+
const q = query.search?.trim();
|
|
197
|
+
if (q) {
|
|
198
|
+
const like = `%${q}%`;
|
|
199
|
+
filters.push(or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like)));
|
|
200
|
+
}
|
|
201
|
+
const where = and(...filters);
|
|
202
|
+
const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
|
|
203
|
+
return {
|
|
204
|
+
rows: (await this.db.select({
|
|
205
|
+
...studentProfileColumns,
|
|
206
|
+
status: orgUsers.status,
|
|
207
|
+
createdAt: orgUsers.createdAt,
|
|
208
|
+
entitlementCount: entitlementCountExpr,
|
|
209
|
+
avgProgress: avgProgressExpr,
|
|
210
|
+
lastActiveAt: lastActiveExpr
|
|
211
|
+
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.id, users.externalId, users.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map(toStudent),
|
|
212
|
+
total: Number(totals?.total ?? 0),
|
|
213
|
+
page: query.page,
|
|
214
|
+
pageSize: query.pageSize
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async getStudent(orgId, id) {
|
|
218
|
+
const [row] = await this.db.select({
|
|
219
|
+
...studentProfileColumns,
|
|
220
|
+
status: orgUsers.status,
|
|
221
|
+
createdAt: orgUsers.createdAt,
|
|
222
|
+
entitlementCount: entitlementCountExpr,
|
|
223
|
+
avgProgress: avgProgressExpr,
|
|
224
|
+
lastActiveAt: lastActiveExpr
|
|
225
|
+
}).from(orgUsers).leftJoin(entitlements, and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id))).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE))).groupBy(orgUsers.orgId, orgUsers.id, orgUsers.status, users.id, users.externalId, users.email, users.firstName, users.lastName, user.image).limit(1);
|
|
226
|
+
return row ? toStudent(row) : null;
|
|
227
|
+
}
|
|
228
|
+
async studentAnalytics(orgId, id) {
|
|
229
|
+
const [exists] = await this.db.select({ id: orgUsers.id }).from(orgUsers).where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)));
|
|
230
|
+
if (!exists) return null;
|
|
231
|
+
const courses = (await this.db.execute(sql`
|
|
232
|
+
with cohort_courses as (
|
|
233
|
+
select distinct c.id, c.title
|
|
234
|
+
from content_access ca
|
|
235
|
+
join courses c on c.org_id = ca.org_id and c.id = ca.content_id
|
|
236
|
+
where ca.org_id = ${orgId}
|
|
237
|
+
and ca.org_user_id = ${id}
|
|
238
|
+
),
|
|
239
|
+
acts as (
|
|
240
|
+
select a.course_id, a.id
|
|
241
|
+
from activities a
|
|
242
|
+
where a.org_id = ${orgId}
|
|
243
|
+
and a.course_id in (select id from cohort_courses)
|
|
244
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
245
|
+
)
|
|
246
|
+
select
|
|
247
|
+
cc.id as course_id,
|
|
248
|
+
cc.title,
|
|
249
|
+
count(a.id)::int as total_activities,
|
|
250
|
+
count(pr.id) filter (where pr.completed_at is not null)::int as completed_activities,
|
|
251
|
+
min(pr.started_at) as started_at,
|
|
252
|
+
max(pr.updated_at) as last_activity_at,
|
|
253
|
+
(
|
|
254
|
+
select prc.completed_at from progress_records prc
|
|
255
|
+
where prc.org_id = ${orgId}
|
|
256
|
+
and prc.org_user_id = ${id}
|
|
257
|
+
and prc.target_type = 'course'
|
|
258
|
+
and prc.target_id = cc.id
|
|
259
|
+
) as completed_at
|
|
260
|
+
from cohort_courses cc
|
|
261
|
+
left join acts a on a.course_id = cc.id
|
|
262
|
+
left join progress_records pr
|
|
263
|
+
on pr.org_id = ${orgId}
|
|
264
|
+
and pr.org_user_id = ${id}
|
|
265
|
+
and pr.target_type = 'activity'
|
|
266
|
+
and pr.target_id = a.id
|
|
267
|
+
group by cc.id, cc.title
|
|
268
|
+
order by cc.title
|
|
269
|
+
`)).rows.map((r) => {
|
|
270
|
+
const total = Number(r.total_activities);
|
|
271
|
+
const done = Number(r.completed_activities);
|
|
272
|
+
return {
|
|
273
|
+
courseId: r.course_id,
|
|
274
|
+
title: r.title,
|
|
275
|
+
totalActivities: total,
|
|
276
|
+
completedActivities: done,
|
|
277
|
+
progress: total > 0 ? Math.round(Math.min(done / total, 1) * 100) : 0,
|
|
278
|
+
startedAt: r.started_at ? new Date(r.started_at).toISOString() : null,
|
|
279
|
+
lastActivityAt: r.last_activity_at ? new Date(r.last_activity_at).toISOString() : null,
|
|
280
|
+
completedAt: r.completed_at ? new Date(r.completed_at).toISOString() : null
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
const started = courses.filter((c) => c.startedAt !== null).length;
|
|
284
|
+
const completed = courses.filter((c) => c.completedAt !== null).length;
|
|
285
|
+
const avgProgress = courses.length > 0 ? Math.round(courses.reduce((sum, c) => sum + c.progress, 0) / courses.length) : 0;
|
|
286
|
+
return {
|
|
287
|
+
enrolled: courses.length,
|
|
288
|
+
started,
|
|
289
|
+
completed,
|
|
290
|
+
avgProgress,
|
|
291
|
+
courses
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
resolveOrder(sort) {
|
|
295
|
+
const descending = sort?.startsWith("-") ?? false;
|
|
296
|
+
const field = sort ? descending ? sort.slice(1) : sort : "firstName";
|
|
297
|
+
const dir = descending ? desc : asc;
|
|
298
|
+
switch (field) {
|
|
299
|
+
case "email": return [dir(users.email)];
|
|
300
|
+
case "entitlementCount": return [dir(entitlementCountExpr)];
|
|
301
|
+
case "avgProgress": return [dir(avgProgressExpr)];
|
|
302
|
+
case "joinedAt": return [dir(orgUsers.createdAt)];
|
|
303
|
+
case "lastActiveAt": return [dir(lastActiveExpr)];
|
|
304
|
+
case "lastName": return [dir(studentLastName), dir(studentFirstName)];
|
|
305
|
+
default: return [dir(studentFirstName), dir(studentLastName)];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async communityAnalytics(orgId, communityId) {
|
|
309
|
+
const [community] = await this.db.select({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), eq(communities.id, communityId))).limit(1);
|
|
310
|
+
if (!community) return null;
|
|
311
|
+
const opens = grantOpens(entitlements, sql`${communityId}`, "community");
|
|
312
|
+
const lapsed = and(eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), grantExpired(entitlements.expiresAt), opens);
|
|
313
|
+
const recent = sql`now() - interval '30 days'`;
|
|
314
|
+
const [[members], [lapsedGrants], authors, postsPerSpace, commentsPerPost, [sent], [postFlagRow], [commentFlagRow], [messageFlagRow], lapsedHolders] = await Promise.all([
|
|
315
|
+
this.db.select({ count: sql`cast(count(distinct ${contentAccess.orgUserId}) as int)` }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId))),
|
|
316
|
+
this.db.select({ count: sql`cast(count(*) as int)` }).from(entitlements).where(lapsed),
|
|
317
|
+
union(this.db.select({ id: posts.orgUserId }).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.communityId, communityId), sql`${posts.createdAt} >= ${recent}`)), this.db.select({ id: comments.orgUserId }).from(comments).where(and(eq(comments.orgId, orgId), eq(comments.communityId, communityId), sql`${comments.createdAt} >= ${recent}`)), this.db.select({ id: messages.orgUserId }).from(messages).where(and(eq(messages.orgId, orgId), eq(messages.communityId, communityId), sql`${messages.createdAt} >= ${recent}`))),
|
|
318
|
+
this.db.select({
|
|
319
|
+
spaceId: spaces.id,
|
|
320
|
+
count: sql`cast(count(${posts.id}) as int)`
|
|
321
|
+
}).from(spaces).leftJoin(posts, and(eq(posts.orgId, spaces.orgId), eq(posts.spaceId, spaces.id), eq(posts.status, "published"))).where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId))).groupBy(spaces.id),
|
|
322
|
+
this.db.select({
|
|
323
|
+
postId: posts.id,
|
|
324
|
+
count: sql`cast(count(${comments.id}) as int)`
|
|
325
|
+
}).from(posts).leftJoin(comments, and(eq(comments.orgId, posts.orgId), eq(comments.postId, posts.id), eq(comments.status, "published"))).where(and(eq(posts.orgId, orgId), eq(posts.communityId, communityId), eq(posts.status, "published"))).groupBy(posts.id),
|
|
326
|
+
this.db.select({ count: sql`cast(count(*) as int)` }).from(messages).where(and(eq(messages.orgId, orgId), eq(messages.communityId, communityId))),
|
|
327
|
+
this.db.select({
|
|
328
|
+
raised: sql`cast(count(*) as int)`,
|
|
329
|
+
resolved: sql`cast(count(*) filter (where ${postFlags.resolvedAt} is not null) as int)`
|
|
330
|
+
}).from(postFlags).innerJoin(posts, and(eq(posts.orgId, postFlags.orgId), eq(posts.id, postFlags.postId))).where(and(eq(postFlags.orgId, orgId), eq(posts.communityId, communityId))),
|
|
331
|
+
this.db.select({
|
|
332
|
+
raised: sql`cast(count(*) as int)`,
|
|
333
|
+
resolved: sql`cast(count(*) filter (where ${commentFlags.resolvedAt} is not null) as int)`
|
|
334
|
+
}).from(commentFlags).innerJoin(comments, and(eq(comments.orgId, commentFlags.orgId), eq(comments.id, commentFlags.commentId))).where(and(eq(commentFlags.orgId, orgId), eq(comments.communityId, communityId))),
|
|
335
|
+
this.db.select({
|
|
336
|
+
raised: sql`cast(count(*) as int)`,
|
|
337
|
+
resolved: sql`cast(count(*) filter (where ${messageFlags.resolvedAt} is not null) as int)`
|
|
338
|
+
}).from(messageFlags).where(and(eq(messageFlags.orgId, orgId), eq(messageFlags.communityId, communityId))),
|
|
339
|
+
this.db.select({ orgUserId: entitlements.orgUserId }).from(entitlements).where(lapsed).groupBy(entitlements.orgUserId)
|
|
340
|
+
]);
|
|
341
|
+
const holderIds = lapsedHolders.map((row) => row.orgUserId);
|
|
342
|
+
const [renewed] = holderIds.length === 0 ? [{ count: 0 }] : await this.db.select({ count: sql`cast(count(distinct ${contentAccess.orgUserId}) as int)` }).from(contentAccess).where(and(eq(contentAccess.orgId, orgId), eq(contentAccess.contentId, communityId), inArray(contentAccess.orgUserId, holderIds)));
|
|
343
|
+
const raised = Number(postFlagRow?.raised ?? 0) + Number(commentFlagRow?.raised ?? 0) + Number(messageFlagRow?.raised ?? 0);
|
|
344
|
+
const resolved = Number(postFlagRow?.resolved ?? 0) + Number(commentFlagRow?.resolved ?? 0) + Number(messageFlagRow?.resolved ?? 0);
|
|
345
|
+
return {
|
|
346
|
+
memberCount: Number(members?.count ?? 0),
|
|
347
|
+
activeMembers: authors.length,
|
|
348
|
+
postsPerSpace: postsPerSpace.map((row) => ({
|
|
349
|
+
spaceId: row.spaceId,
|
|
350
|
+
count: Number(row.count)
|
|
351
|
+
})),
|
|
352
|
+
commentsPerPost: commentsPerPost.map((row) => ({
|
|
353
|
+
postId: row.postId,
|
|
354
|
+
count: Number(row.count)
|
|
355
|
+
})),
|
|
356
|
+
messagesSent: Number(sent?.count ?? 0),
|
|
357
|
+
flagsRaised: raised,
|
|
358
|
+
flagsResolved: resolved,
|
|
359
|
+
grantsLapsed: Number(lapsedGrants?.count ?? 0),
|
|
360
|
+
grantsRenewed: Number(renewed?.count ?? 0)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async communityMembers(orgId, communityId, query) {
|
|
364
|
+
const [community] = await this.db.select({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), eq(communities.id, communityId))).limit(1);
|
|
365
|
+
if (!community) return null;
|
|
366
|
+
const holds = exists(this.db.select({ id: contentAccess.entitlementId }).from(contentAccess).where(and(eq(contentAccess.orgId, orgUsers.orgId), eq(contentAccess.orgUserId, orgUsers.id), eq(contentAccess.contentId, communityId))));
|
|
367
|
+
const filters = [eq(orgUsers.orgId, orgId), holds];
|
|
368
|
+
const q = query.search?.trim();
|
|
369
|
+
if (q) {
|
|
370
|
+
const like = `%${q}%`;
|
|
371
|
+
filters.push(or(ilike(studentFirstName, like), ilike(studentLastName, like), ilike(users.email, like)));
|
|
372
|
+
}
|
|
373
|
+
const where = and(...filters);
|
|
374
|
+
const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
|
|
375
|
+
return {
|
|
376
|
+
rows: (await this.db.select({
|
|
377
|
+
...orgUserProfileColumns,
|
|
378
|
+
role: orgUsers.role
|
|
379
|
+
}).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).leftJoin(user, eq(user.id, users.externalId)).where(where).orderBy(asc(users.firstName), asc(users.lastName), asc(orgUsers.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map((row) => ({
|
|
380
|
+
...row,
|
|
381
|
+
image: row.image ?? null
|
|
382
|
+
})),
|
|
383
|
+
total: Number(totals?.total ?? 0),
|
|
384
|
+
page: query.page,
|
|
385
|
+
pageSize: query.pageSize
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
translateDbErrors(DrizzleAdminRepository);
|
|
390
|
+
//#endregion
|
|
391
|
+
export { DrizzleAdminRepository };
|
|
392
|
+
|
|
393
|
+
//# sourceMappingURL=admin.js.map
|