@headless-lms/adapter-db 0.1.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/LICENSE +21 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.js +11 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/migrate.d.ts +7 -0
- package/dist/migrate.js +27 -0
- package/dist/migrate.js.map +1 -0
- package/dist/repositories/assets.d.ts +17 -0
- package/dist/repositories/assets.js +90 -0
- package/dist/repositories/assets.js.map +1 -0
- package/dist/repositories/automations.d.ts +30 -0
- package/dist/repositories/automations.js +165 -0
- package/dist/repositories/automations.js.map +1 -0
- package/dist/repositories/content.d.ts +52 -0
- package/dist/repositories/content.js +350 -0
- package/dist/repositories/content.js.map +1 -0
- package/dist/repositories/credentials.d.ts +25 -0
- package/dist/repositories/credentials.js +86 -0
- package/dist/repositories/credentials.js.map +1 -0
- package/dist/repositories/dashboard.d.ts +14 -0
- package/dist/repositories/dashboard.js +55 -0
- package/dist/repositories/dashboard.js.map +1 -0
- package/dist/repositories/discussion.d.ts +31 -0
- package/dist/repositories/discussion.js +201 -0
- package/dist/repositories/discussion.js.map +1 -0
- package/dist/repositories/entitlements.d.ts +23 -0
- package/dist/repositories/entitlements.js +129 -0
- package/dist/repositories/entitlements.js.map +1 -0
- package/dist/repositories/identity.d.ts +17 -0
- package/dist/repositories/identity.js +44 -0
- package/dist/repositories/identity.js.map +1 -0
- package/dist/repositories/integrations.d.ts +18 -0
- package/dist/repositories/integrations.js +65 -0
- package/dist/repositories/integrations.js.map +1 -0
- package/dist/repositories/learn.d.ts +19 -0
- package/dist/repositories/learn.js +54 -0
- package/dist/repositories/learn.js.map +1 -0
- package/dist/repositories/members.d.ts +16 -0
- package/dist/repositories/members.js +116 -0
- package/dist/repositories/members.js.map +1 -0
- package/dist/repositories/org-user-profile.d.ts +92 -0
- package/dist/repositories/org-user-profile.js +16 -0
- package/dist/repositories/org-user-profile.js.map +1 -0
- package/dist/repositories/organizations.d.ts +33 -0
- package/dist/repositories/organizations.js +198 -0
- package/dist/repositories/organizations.js.map +1 -0
- package/dist/repositories/outbox.d.ts +24 -0
- package/dist/repositories/outbox.js +56 -0
- package/dist/repositories/outbox.js.map +1 -0
- package/dist/repositories/pg-errors.d.ts +11 -0
- package/dist/repositories/pg-errors.js +15 -0
- package/dist/repositories/pg-errors.js.map +1 -0
- package/dist/repositories/progress.d.ts +18 -0
- package/dist/repositories/progress.js +59 -0
- package/dist/repositories/progress.js.map +1 -0
- package/dist/repositories/settings.d.ts +22 -0
- package/dist/repositories/settings.js +73 -0
- package/dist/repositories/settings.js.map +1 -0
- package/dist/repositories/students.d.ts +15 -0
- package/dist/repositories/students.js +83 -0
- package/dist/repositories/students.js.map +1 -0
- package/dist/schema/assets.d.ts +181 -0
- package/dist/schema/assets.js +24 -0
- package/dist/schema/assets.js.map +1 -0
- package/dist/schema/automations.d.ts +364 -0
- package/dist/schema/automations.js +42 -0
- package/dist/schema/automations.js.map +1 -0
- package/dist/schema/better-auth.d.ts +2311 -0
- package/dist/schema/better-auth.js +156 -0
- package/dist/schema/better-auth.js.map +1 -0
- package/dist/schema/content.d.ts +966 -0
- package/dist/schema/content.js +163 -0
- package/dist/schema/content.js.map +1 -0
- package/dist/schema/credentials.d.ts +113 -0
- package/dist/schema/credentials.js +15 -0
- package/dist/schema/credentials.js.map +1 -0
- package/dist/schema/discussion.d.ts +399 -0
- package/dist/schema/discussion.js +89 -0
- package/dist/schema/discussion.js.map +1 -0
- package/dist/schema/drift.d.ts +9 -0
- package/dist/schema/drift.js +1 -0
- package/dist/schema/entitlements.d.ts +147 -0
- package/dist/schema/entitlements.js +30 -0
- package/dist/schema/entitlements.js.map +1 -0
- package/dist/schema/identity.d.ts +130 -0
- package/dist/schema/identity.js +16 -0
- package/dist/schema/identity.js.map +1 -0
- package/dist/schema/index.d.ts +18 -0
- package/dist/schema/index.js +43 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/integrations.d.ts +149 -0
- package/dist/schema/integrations.js +25 -0
- package/dist/schema/integrations.js.map +1 -0
- package/dist/schema/organizations.d.ts +416 -0
- package/dist/schema/organizations.js +58 -0
- package/dist/schema/organizations.js.map +1 -0
- package/dist/schema/outbox.d.ts +166 -0
- package/dist/schema/outbox.js +19 -0
- package/dist/schema/outbox.js.map +1 -0
- package/dist/schema/progress.d.ts +166 -0
- package/dist/schema/progress.js +30 -0
- package/dist/schema/progress.js.map +1 -0
- package/dist/schema/settings.d.ts +115 -0
- package/dist/schema/settings.js +19 -0
- package/dist/schema/settings.js.map +1 -0
- package/dist/unit-of-work.d.ts +13 -0
- package/dist/unit-of-work.js +16 -0
- package/dist/unit-of-work.js.map +1 -0
- package/drizzle/0000_baseline.sql +526 -0
- package/drizzle/meta/0000_snapshot.json +3925 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +56 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { eventOutbox } from "../schema/outbox.js";
|
|
2
|
+
import { and, asc, eq, isNull, lt, lte, sql } from "drizzle-orm";
|
|
3
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
|
+
//#region src/repositories/outbox.ts
|
|
5
|
+
/** A message that fails this many dispatches is parked: fetchBatch stops
|
|
6
|
+
* claiming it, leaving the row (with its last_error) for inspection. */
|
|
7
|
+
const OUTBOX_MAX_ATTEMPTS = 10;
|
|
8
|
+
var DrizzleOutboxAppender = class {
|
|
9
|
+
tx;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(tx, logger = noopLogger) {
|
|
12
|
+
this.tx = tx;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
async append(events) {
|
|
16
|
+
if (events.length === 0) return;
|
|
17
|
+
await this.tx.insert(eventOutbox).values(events.map((event) => ({
|
|
18
|
+
type: event.type,
|
|
19
|
+
orgId: event.orgId,
|
|
20
|
+
payload: event
|
|
21
|
+
})));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var DrizzleOutboxStore = class {
|
|
25
|
+
db;
|
|
26
|
+
logger;
|
|
27
|
+
constructor(db, logger = noopLogger) {
|
|
28
|
+
this.db = db;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
async fetchBatch(limit) {
|
|
32
|
+
return (await this.db.transaction((tx) => tx.select().from(eventOutbox).where(and(isNull(eventOutbox.processedAt), lte(eventOutbox.nextAttemptAt, /* @__PURE__ */ new Date()), lt(eventOutbox.attempts, 10))).orderBy(asc(eventOutbox.id)).limit(limit).for("update", { skipLocked: true }))).map((row) => ({
|
|
33
|
+
id: row.id,
|
|
34
|
+
attempts: row.attempts,
|
|
35
|
+
payload: {
|
|
36
|
+
...row.payload,
|
|
37
|
+
id: row.id,
|
|
38
|
+
occurredAt: row.createdAt.toISOString()
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
async markProcessed(id) {
|
|
43
|
+
await this.db.update(eventOutbox).set({ processedAt: /* @__PURE__ */ new Date() }).where(eq(eventOutbox.id, id));
|
|
44
|
+
}
|
|
45
|
+
async markFailed(id, error, nextAttemptAt) {
|
|
46
|
+
await this.db.update(eventOutbox).set({
|
|
47
|
+
attempts: sql`${eventOutbox.attempts} + 1`,
|
|
48
|
+
lastError: error,
|
|
49
|
+
nextAttemptAt
|
|
50
|
+
}).where(eq(eventOutbox.id, id));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { DrizzleOutboxAppender, DrizzleOutboxStore, OUTBOX_MAX_ATTEMPTS };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=outbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox.js","names":[],"sources":["../../src/repositories/outbox.ts"],"sourcesContent":["import { and, asc, eq, isNull, lt, lte, sql } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n DomainEvent,\n NewDomainEvent,\n OutboxAppender,\n OutboxMessage,\n OutboxStore,\n Logger,\n} from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport type { EventOutbox } from '@headless-lms/core/schemas';\nimport type { DbExecutor } from '../client.js';\nimport { eventOutbox } from '../schema/outbox.js';\n\n/** A message that fails this many dispatches is parked: fetchBatch stops\n * claiming it, leaving the row (with its last_error) for inspection. */\nexport const OUTBOX_MAX_ATTEMPTS = 10;\n\nexport class DrizzleOutboxAppender implements OutboxAppender {\n constructor(\n private readonly tx: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async append<E extends NewDomainEvent>(events: E[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n await this.tx.insert(eventOutbox).values(\n events.map((event) => ({\n type: event.type,\n orgId: event.orgId,\n payload: event as unknown as EventOutbox['payload'],\n })),\n );\n }\n}\n\nexport class DrizzleOutboxStore implements OutboxStore {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async fetchBatch(limit: number): Promise<OutboxMessage[]> {\n const rows = await this.db.transaction((tx) =>\n tx\n .select()\n .from(eventOutbox)\n .where(\n and(\n isNull(eventOutbox.processedAt),\n lte(eventOutbox.nextAttemptAt, new Date()),\n lt(eventOutbox.attempts, OUTBOX_MAX_ATTEMPTS),\n ),\n )\n .orderBy(asc(eventOutbox.id))\n .limit(limit)\n .for('update', { skipLocked: true }),\n );\n return rows.map((row) => ({\n id: row.id,\n attempts: row.attempts,\n payload: {\n ...row.payload,\n id: row.id,\n occurredAt: row.createdAt.toISOString(),\n } as unknown as DomainEvent,\n }));\n }\n\n async markProcessed(id: string): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({ processedAt: new Date() })\n .where(eq(eventOutbox.id, id));\n }\n\n async markFailed(id: string, error: string, nextAttemptAt: Date): Promise<void> {\n await this.db\n .update(eventOutbox)\n .set({\n attempts: sql`${eventOutbox.attempts} + 1`,\n lastError: error,\n nextAttemptAt,\n })\n .where(eq(eventOutbox.id, id));\n }\n}\n"],"mappings":";;;;;;AAiBA,MAAa,sBAAsB;AAEnC,IAAa,wBAAb,MAA6D;CAExC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAiC,QAA4B;EACjE,IAAI,OAAO,WAAW,GACpB;EAEF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAChC,OAAO,KAAK,WAAW;GACrB,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,SAAS;EACX,EAAE,CACJ;CACF;AACF;AAEA,IAAa,qBAAb,MAAuD;CAElC;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,WAAW,OAAyC;EAgBxD,QAAO,MAfY,KAAK,GAAG,aAAa,OACtC,GACG,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MACC,IACE,OAAO,YAAY,WAAW,GAC9B,IAAI,YAAY,+BAAe,IAAI,KAAK,CAAC,GACzC,GAAG,YAAY,UAAA,EAA6B,CAC9C,CACF,CAAC,CACA,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAC,CAC5B,MAAM,KAAK,CAAC,CACZ,IAAI,UAAU,EAAE,YAAY,KAAK,CAAC,CACvC,EAAA,CACY,KAAK,SAAS;GACxB,IAAI,IAAI;GACR,UAAU,IAAI;GACd,SAAS;IACP,GAAG,IAAI;IACP,IAAI,IAAI;IACR,YAAY,IAAI,UAAU,YAAY;GACxC;EACF,EAAE;CACJ;CAEA,MAAM,cAAc,IAA2B;EAC7C,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI,EAAE,6BAAa,IAAI,KAAK,EAAE,CAAC,CAAC,CAChC,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;CAEA,MAAM,WAAW,IAAY,OAAe,eAAoC;EAC9E,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GACH,UAAU,GAAG,GAAG,YAAY,SAAS;GACrC,WAAW;GACX;EACF,CAAC,CAAC,CACD,MAAM,GAAG,YAAY,IAAI,EAAE,CAAC;CACjC;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/repositories/pg-errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Postgres unique_violation (23505).
|
|
4
|
+
*
|
|
5
|
+
* The node-postgres driver sometimes wraps the original error (e.g. behind
|
|
6
|
+
* `cause`), so both levels are checked.
|
|
7
|
+
*/
|
|
8
|
+
declare function isUniqueViolation(err: unknown): boolean;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { isUniqueViolation };
|
|
11
|
+
//# sourceMappingURL=pg-errors.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/repositories/pg-errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Postgres unique_violation (23505).
|
|
4
|
+
*
|
|
5
|
+
* The node-postgres driver sometimes wraps the original error (e.g. behind
|
|
6
|
+
* `cause`), so both levels are checked.
|
|
7
|
+
*/
|
|
8
|
+
function isUniqueViolation(err) {
|
|
9
|
+
if (err?.code === "23505") return true;
|
|
10
|
+
return (err?.cause)?.code === "23505";
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { isUniqueViolation };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=pg-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-errors.js","names":[],"sources":["../../src/repositories/pg-errors.ts"],"sourcesContent":["/**\n * Postgres unique_violation (23505).\n *\n * The node-postgres driver sometimes wraps the original error (e.g. behind\n * `cause`), so both levels are checked.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n const code = (err as { code?: unknown } | undefined)?.code;\n if (code === '23505') {\n return true;\n }\n const cause = (err as { cause?: unknown } | undefined)?.cause;\n const causeCode = (cause as { code?: unknown } | undefined)?.code;\n return causeCode === '23505';\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,kBAAkB,KAAuB;CAEvD,IADc,KAAwC,SACzC,SACX,OAAO;CAIT,QAFe,KAAyC,MAAA,EACK,SACxC;AACvB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
|
+
import { ProgressRecord, ProgressRepository, ProgressTarget } from "@headless-lms/core/progress";
|
|
4
|
+
//#region src/repositories/progress.d.ts
|
|
5
|
+
declare class DrizzleProgressRepository implements ProgressRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
|
+
insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null>;
|
|
10
|
+
findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null>;
|
|
11
|
+
findByTargets(orgId: string, orgUserId: string, targetIds: string[], opts?: {
|
|
12
|
+
forUpdate?: boolean;
|
|
13
|
+
}): Promise<ProgressRecord[]>;
|
|
14
|
+
update(orgId: string, id: string, patch: Partial<Pick<ProgressRecord, 'position' | 'completedAt'>>): Promise<ProgressRecord | null>;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { DrizzleProgressRepository };
|
|
18
|
+
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { progressRecords } from "../schema/progress.js";
|
|
2
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
3
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
|
+
//#region src/repositories/progress.ts
|
|
5
|
+
function toRecord(row) {
|
|
6
|
+
return {
|
|
7
|
+
id: row.id,
|
|
8
|
+
orgId: row.orgId,
|
|
9
|
+
orgUserId: row.orgUserId,
|
|
10
|
+
targetType: row.targetType,
|
|
11
|
+
targetId: row.targetId,
|
|
12
|
+
startedAt: row.startedAt,
|
|
13
|
+
position: row.position ?? null,
|
|
14
|
+
completedAt: row.completedAt,
|
|
15
|
+
updatedAt: row.updatedAt
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var DrizzleProgressRepository = class {
|
|
19
|
+
db;
|
|
20
|
+
logger;
|
|
21
|
+
constructor(db, logger = noopLogger) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
async insert(orgId, record) {
|
|
26
|
+
const [row] = await this.db.insert(progressRecords).values({
|
|
27
|
+
id: record.id,
|
|
28
|
+
orgId,
|
|
29
|
+
orgUserId: record.orgUserId,
|
|
30
|
+
targetType: record.targetType,
|
|
31
|
+
targetId: record.targetId,
|
|
32
|
+
startedAt: new Date(record.startedAt),
|
|
33
|
+
position: record.position ?? null,
|
|
34
|
+
completedAt: record.completedAt ? new Date(record.completedAt) : null,
|
|
35
|
+
updatedAt: new Date(record.updatedAt)
|
|
36
|
+
}).onConflictDoNothing().returning();
|
|
37
|
+
return row ? toRecord(row) : null;
|
|
38
|
+
}
|
|
39
|
+
async findByTarget(orgId, target) {
|
|
40
|
+
const [row] = await this.db.select().from(progressRecords).where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.orgUserId, target.orgUserId), eq(progressRecords.targetType, target.targetType), eq(progressRecords.targetId, target.targetId))).limit(1);
|
|
41
|
+
return row ? toRecord(row) : null;
|
|
42
|
+
}
|
|
43
|
+
async findByTargets(orgId, orgUserId, targetIds, opts) {
|
|
44
|
+
if (targetIds.length === 0) return [];
|
|
45
|
+
const base = this.db.select().from(progressRecords).where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.orgUserId, orgUserId), inArray(progressRecords.targetId, targetIds)));
|
|
46
|
+
return (opts?.forUpdate ? await base.orderBy(progressRecords.id).for("update") : await base).map(toRecord);
|
|
47
|
+
}
|
|
48
|
+
async update(orgId, id, patch) {
|
|
49
|
+
const [row] = await this.db.update(progressRecords).set({
|
|
50
|
+
..."position" in patch ? { position: patch.position ?? null } : {},
|
|
51
|
+
..."completedAt" in patch ? { completedAt: patch.completedAt ? new Date(patch.completedAt) : null } : {}
|
|
52
|
+
}).where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.id, id))).returning();
|
|
53
|
+
return row ? toRecord(row) : null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { DrizzleProgressRepository };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.js","names":[],"sources":["../../src/repositories/progress.ts"],"sourcesContent":["// progress — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n ProgressRecord,\n ProgressRepository,\n ProgressTarget,\n ProgressTargetType,\n} from '@headless-lms/core/progress';\nimport { progressRecords } from '../schema/progress.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\ntype Row = typeof progressRecords.$inferSelect;\n\nfunction toRecord(row: Row): ProgressRecord {\n return {\n id: row.id,\n orgId: row.orgId,\n orgUserId: row.orgUserId,\n targetType: row.targetType as ProgressTargetType,\n targetId: row.targetId,\n startedAt: row.startedAt,\n position: row.position ?? null,\n completedAt: row.completedAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleProgressRepository implements ProgressRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, record: ProgressRecord): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .insert(progressRecords)\n .values({\n id: record.id,\n orgId,\n orgUserId: record.orgUserId,\n targetType: record.targetType,\n targetId: record.targetId,\n startedAt: new Date(record.startedAt),\n position: record.position ?? null,\n completedAt: record.completedAt ? new Date(record.completedAt) : null,\n updatedAt: new Date(record.updatedAt),\n })\n .onConflictDoNothing()\n .returning();\n return row ? toRecord(row) : null;\n }\n\n async findByTarget(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, target.orgUserId),\n eq(progressRecords.targetType, target.targetType),\n eq(progressRecords.targetId, target.targetId),\n ),\n )\n .limit(1);\n return row ? toRecord(row) : null;\n }\n\n async findByTargets(\n orgId: string,\n orgUserId: string,\n targetIds: string[],\n opts?: { forUpdate?: boolean },\n ): Promise<ProgressRecord[]> {\n if (targetIds.length === 0) {\n return [];\n }\n const base = this.db\n .select()\n .from(progressRecords)\n .where(\n and(\n eq(progressRecords.orgId, orgId),\n eq(progressRecords.orgUserId, orgUserId),\n inArray(progressRecords.targetId, targetIds),\n ),\n );\n // Deterministic lock order prevents deadlock between overlapping reports.\n const rows = opts?.forUpdate\n ? await base.orderBy(progressRecords.id).for('update')\n : await base;\n return rows.map(toRecord);\n }\n\n async update(\n orgId: string,\n id: string,\n patch: Partial<Pick<ProgressRecord, 'position' | 'completedAt'>>,\n ): Promise<ProgressRecord | null> {\n const [row] = await this.db\n .update(progressRecords)\n .set({\n ...('position' in patch ? { position: patch.position ?? null } : {}),\n ...('completedAt' in patch\n ? { completedAt: patch.completedAt ? new Date(patch.completedAt) : null }\n : {}),\n })\n .where(and(eq(progressRecords.orgId, orgId), eq(progressRecords.id, id)))\n .returning();\n return row ? toRecord(row) : null;\n }\n}\n"],"mappings":";;;;AAeA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,IAAI,IAAI;EACR,OAAO,IAAI;EACX,WAAW,IAAI;EACf,YAAY,IAAI;EAChB,UAAU,IAAI;EACd,WAAW,IAAI;EACf,UAAU,IAAI,YAAY;EAC1B,aAAa,IAAI;EACjB,WAAW,IAAI;CACjB;AACF;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OAAO,OAAe,QAAwD;EAClF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,OAAO;GACN,IAAI,OAAO;GACX;GACA,WAAW,OAAO;GAClB,YAAY,OAAO;GACnB,UAAU,OAAO;GACjB,WAAW,IAAI,KAAK,OAAO,SAAS;GACpC,UAAU,OAAO,YAAY;GAC7B,aAAa,OAAO,cAAc,IAAI,KAAK,OAAO,WAAW,IAAI;GACjE,WAAW,IAAI,KAAK,OAAO,SAAS;EACtC,CAAC,CAAC,CACD,oBAAoB,CAAC,CACrB,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,aAAa,OAAe,QAAwD;EACxF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,OAAO,SAAS,GAC9C,GAAG,gBAAgB,YAAY,OAAO,UAAU,GAChD,GAAG,gBAAgB,UAAU,OAAO,QAAQ,CAC9C,CACF,CAAC,CACA,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,cACJ,OACA,WACA,WACA,MAC2B;EAC3B,IAAI,UAAU,WAAW,GACvB,OAAO,CAAC;EAEV,MAAM,OAAO,KAAK,GACf,OAAO,CAAC,CACR,KAAK,eAAe,CAAC,CACrB,MACC,IACE,GAAG,gBAAgB,OAAO,KAAK,GAC/B,GAAG,gBAAgB,WAAW,SAAS,GACvC,QAAQ,gBAAgB,UAAU,SAAS,CAC7C,CACF;EAKF,QAHa,MAAM,YACf,MAAM,KAAK,QAAQ,gBAAgB,EAAE,CAAC,CAAC,IAAI,QAAQ,IACnD,MAAM,KAAA,CACE,IAAI,QAAQ;CAC1B;CAEA,MAAM,OACJ,OACA,IACA,OACgC;EAChC,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,eAAe,CAAC,CACvB,IAAI;GACH,GAAI,cAAc,QAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,CAAC;GAClE,GAAI,iBAAiB,QACjB,EAAE,aAAa,MAAM,cAAc,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,IACtE,CAAC;EACP,CAAC,CAAC,CACD,MAAM,IAAI,GAAG,gBAAgB,OAAO,KAAK,GAAG,GAAG,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CACxE,UAAU;EACb,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;AACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger } from "@headless-lms/core/types";
|
|
3
|
+
import { SettingsRecord, SettingsRepository } from "@headless-lms/core/shared/settings";
|
|
4
|
+
//#region src/repositories/settings.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Recursive merge of a partial patch into a stored value: a patch cannot
|
|
7
|
+
* clobber sibling keys at any depth, only the leaves it names. Arrays and
|
|
8
|
+
* scalars are replaced outright — merging arrays element-wise has no meaning
|
|
9
|
+
* for settings, and null is a value a caller may legitimately store.
|
|
10
|
+
* Exported for tests.
|
|
11
|
+
*/
|
|
12
|
+
declare function deepMerge(base: unknown, patch: unknown): unknown;
|
|
13
|
+
declare class DrizzleSettingsRepository implements SettingsRepository {
|
|
14
|
+
private readonly db;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
17
|
+
find(orgId: string, scopeId: string, namespace?: string): Promise<SettingsRecord[]>;
|
|
18
|
+
patch(orgId: string, namespace: string, scopeId: string, patch: Record<string, unknown>): Promise<SettingsRecord>;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DrizzleSettingsRepository, deepMerge };
|
|
22
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { settings } from "../schema/settings.js";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
4
|
+
//#region src/repositories/settings.ts
|
|
5
|
+
function toRecord(row) {
|
|
6
|
+
return {
|
|
7
|
+
namespace: row.namespace,
|
|
8
|
+
scopeId: row.scopeId,
|
|
9
|
+
value: row.value
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recursive merge of a partial patch into a stored value: a patch cannot
|
|
17
|
+
* clobber sibling keys at any depth, only the leaves it names. Arrays and
|
|
18
|
+
* scalars are replaced outright — merging arrays element-wise has no meaning
|
|
19
|
+
* for settings, and null is a value a caller may legitimately store.
|
|
20
|
+
* Exported for tests.
|
|
21
|
+
*/
|
|
22
|
+
function deepMerge(base, patch) {
|
|
23
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
24
|
+
const merged = { ...base };
|
|
25
|
+
for (const [key, value] of Object.entries(patch)) merged[key] = key in base ? deepMerge(base[key], value) : value;
|
|
26
|
+
return merged;
|
|
27
|
+
}
|
|
28
|
+
var DrizzleSettingsRepository = class {
|
|
29
|
+
db;
|
|
30
|
+
logger;
|
|
31
|
+
constructor(db, logger = noopLogger) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
}
|
|
35
|
+
async find(orgId, scopeId, namespace) {
|
|
36
|
+
const baseConditions = [eq(settings.orgId, orgId), eq(settings.scopeId, scopeId)];
|
|
37
|
+
const where = namespace ? and(...baseConditions, eq(settings.namespace, namespace)) : and(...baseConditions);
|
|
38
|
+
return (await this.db.select().from(settings).where(where)).map(toRecord);
|
|
39
|
+
}
|
|
40
|
+
async patch(orgId, namespace, scopeId, patch) {
|
|
41
|
+
const key = and(eq(settings.orgId, orgId), eq(settings.namespace, namespace), eq(settings.scopeId, scopeId));
|
|
42
|
+
const row = await this.db.transaction(async (tx) => {
|
|
43
|
+
let [existing] = await tx.select().from(settings).where(key).for("update");
|
|
44
|
+
if (!existing) {
|
|
45
|
+
const [inserted] = await tx.insert(settings).values({
|
|
46
|
+
orgId,
|
|
47
|
+
namespace,
|
|
48
|
+
scopeId,
|
|
49
|
+
value: patch
|
|
50
|
+
}).onConflictDoNothing().returning();
|
|
51
|
+
if (inserted) return inserted;
|
|
52
|
+
[existing] = await tx.select().from(settings).where(key).for("update");
|
|
53
|
+
if (!existing) throw new Error("settings patch: row vanished mid-transaction");
|
|
54
|
+
}
|
|
55
|
+
const [updated] = await tx.update(settings).set({
|
|
56
|
+
value: deepMerge(existing.value, patch),
|
|
57
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
58
|
+
}).where(key).returning();
|
|
59
|
+
return updated;
|
|
60
|
+
});
|
|
61
|
+
if (!row) throw new Error("settings patch returned no row");
|
|
62
|
+
this.logger.debug("settings.patch", {
|
|
63
|
+
orgId,
|
|
64
|
+
namespace,
|
|
65
|
+
scopeId
|
|
66
|
+
});
|
|
67
|
+
return toRecord(row);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { DrizzleSettingsRepository, deepMerge };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","names":[],"sources":["../../src/repositories/settings.ts"],"sourcesContent":["import { and, eq } from 'drizzle-orm';\nimport type { Setting } from '@headless-lms/core/schemas';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { SettingsRecord, SettingsRepository } from '@headless-lms/core/shared/settings';\nimport { settings } from '../schema/settings.js';\nimport type { Logger } from '@headless-lms/core/types';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\ntype Row = typeof settings.$inferSelect;\n\nfunction toRecord(row: Row): SettingsRecord {\n return {\n namespace: row.namespace,\n scopeId: row.scopeId,\n value: row.value,\n };\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursive merge of a partial patch into a stored value: a patch cannot\n * clobber sibling keys at any depth, only the leaves it names. Arrays and\n * scalars are replaced outright — merging arrays element-wise has no meaning\n * for settings, and null is a value a caller may legitimately store.\n * Exported for tests.\n */\nexport function deepMerge(base: unknown, patch: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(patch)) {\n return patch;\n }\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(patch)) {\n merged[key] = key in base ? deepMerge(base[key], value) : value;\n }\n return merged;\n}\n\nexport class DrizzleSettingsRepository implements SettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async find(orgId: string, scopeId: string, namespace?: string): Promise<SettingsRecord[]> {\n const baseConditions = [eq(settings.orgId, orgId), eq(settings.scopeId, scopeId)];\n const where = namespace\n ? and(...baseConditions, eq(settings.namespace, namespace))\n : and(...baseConditions);\n\n const rows = await this.db.select().from(settings).where(where);\n return rows.map(toRecord);\n }\n\n async patch(\n orgId: string,\n namespace: string,\n scopeId: string,\n patch: Record<string, unknown>,\n ): Promise<SettingsRecord> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, namespace),\n eq(settings.scopeId, scopeId),\n );\n\n // The merge happens in JS, so the read and the write must be one atomic\n // unit or concurrent patches would clobber each other's sibling keys:\n // SELECT ... FOR UPDATE holds the row for the rest of the transaction.\n const row = await this.db.transaction(async (tx) => {\n let [existing] = await tx.select().from(settings).where(key).for('update');\n\n if (!existing) {\n const [inserted] = await tx\n .insert(settings)\n .values({ orgId, namespace, scopeId, value: patch as Setting['value'] })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted;\n }\n // Lost the race: a concurrent transaction inserted the row and has\n // committed by the time onConflictDoNothing returned. Take the lock\n // and merge onto what it wrote.\n [existing] = await tx.select().from(settings).where(key).for('update');\n if (!existing) {\n throw new Error('settings patch: row vanished mid-transaction');\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, patch) as Setting['value'], updatedAt: new Date() })\n .where(key)\n .returning();\n return updated;\n });\n\n if (!row) {\n throw new Error('settings patch returned no row');\n }\n this.logger.debug('settings.patch', { orgId, namespace, scopeId });\n return toRecord(row);\n }\n}\n"],"mappings":";;;;AAUA,SAAS,SAAS,KAA0B;CAC1C,OAAO;EACL,WAAW,IAAI;EACf,SAAS,IAAI;EACb,OAAO,IAAI;CACb;AACF;AAEA,SAAS,cAAc,OAAkD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;;;AASA,SAAgB,UAAU,MAAe,OAAyB;CAChE,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,KAAK,GAC9C,OAAO;CAET,MAAM,SAAkC,EAAE,GAAG,KAAK;CAClD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,OAAO,OAAO,OAAO,OAAO,UAAU,KAAK,MAAM,KAAK,IAAI;CAE5D,OAAO;AACT;AAEA,IAAa,4BAAb,MAAqE;CAEhD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,KAAK,OAAe,SAAiB,WAA+C;EACxF,MAAM,iBAAiB,CAAC,GAAG,SAAS,OAAO,KAAK,GAAG,GAAG,SAAS,SAAS,OAAO,CAAC;EAChF,MAAM,QAAQ,YACV,IAAI,GAAG,gBAAgB,GAAG,SAAS,WAAW,SAAS,CAAC,IACxD,IAAI,GAAG,cAAc;EAGzB,QAAO,MADY,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,KAAK,EAAA,CAClD,IAAI,QAAQ;CAC1B;CAEA,MAAM,MACJ,OACA,WACA,SACA,OACyB;EACzB,MAAM,MAAM,IACV,GAAG,SAAS,OAAO,KAAK,GACxB,GAAG,SAAS,WAAW,SAAS,GAChC,GAAG,SAAS,SAAS,OAAO,CAC9B;EAKA,MAAM,MAAM,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;GAClD,IAAI,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;GAEzE,IAAI,CAAC,UAAU;IACb,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,QAAQ,CAAC,CAChB,OAAO;KAAE;KAAO;KAAW;KAAS,OAAO;IAA0B,CAAC,CAAC,CACvE,oBAAoB,CAAC,CACrB,UAAU;IACb,IAAI,UACF,OAAO;IAKT,CAAC,YAAY,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ;IACrE,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,8CAA8C;GAElE;GAEA,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,QAAQ,CAAC,CAChB,IAAI;IAAE,OAAO,UAAU,SAAS,OAAO,KAAK;IAAuB,2BAAW,IAAI,KAAK;GAAE,CAAC,CAAC,CAC3F,MAAM,GAAG,CAAC,CACV,UAAU;GACb,OAAO;EACT,CAAC;EAED,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,gCAAgC;EAElD,KAAK,OAAO,MAAM,kBAAkB;GAAE;GAAO;GAAW;EAAQ,CAAC;EACjE,OAAO,SAAS,GAAG;CACrB;AACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
|
+
import { Page, Student, StudentsQuery, StudentsReportRepository } from "@headless-lms/core/reporting/students";
|
|
4
|
+
//#region src/repositories/students.d.ts
|
|
5
|
+
declare class DrizzleStudentsRepository implements StudentsReportRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
|
+
list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
|
|
10
|
+
findById(orgId: string, id: string): Promise<Student | null>;
|
|
11
|
+
private resolveOrder;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DrizzleStudentsRepository };
|
|
15
|
+
//# sourceMappingURL=students.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { users } from "../schema/identity.js";
|
|
2
|
+
import { orgUsers } from "../schema/organizations.js";
|
|
3
|
+
import { entitlements } from "../schema/entitlements.js";
|
|
4
|
+
import "../schema/index.js";
|
|
5
|
+
import { user } from "../schema/better-auth.js";
|
|
6
|
+
import { orgUserProfileColumns } from "./org-user-profile.js";
|
|
7
|
+
import { and, asc, desc, eq, ilike, or, sql } from "drizzle-orm";
|
|
8
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
9
|
+
import { STUDENT_ROLE } from "@headless-lms/core/organizations";
|
|
10
|
+
//#region src/repositories/students.ts
|
|
11
|
+
const entitlementCountExpr = sql`count(${entitlements.id})`;
|
|
12
|
+
const avgProgressExpr = sql`0`;
|
|
13
|
+
function toStudent(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
email: row.email,
|
|
17
|
+
image: row.image ?? null,
|
|
18
|
+
firstName: row.firstName,
|
|
19
|
+
lastName: row.lastName,
|
|
20
|
+
status: row.status,
|
|
21
|
+
entitlementCount: Number(row.entitlementCount),
|
|
22
|
+
avgProgress: Number(row.avgProgress),
|
|
23
|
+
joinedAt: row.createdAt.toISOString(),
|
|
24
|
+
lastActiveAt: null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
var DrizzleStudentsRepository = class {
|
|
28
|
+
db;
|
|
29
|
+
logger;
|
|
30
|
+
constructor(db, logger = noopLogger) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
}
|
|
34
|
+
async list(orgId, query) {
|
|
35
|
+
const filters = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];
|
|
36
|
+
const q = query.search?.trim();
|
|
37
|
+
if (q) {
|
|
38
|
+
const like = `%${q}%`;
|
|
39
|
+
filters.push(or(ilike(users.firstName, like), ilike(users.lastName, like), ilike(users.email, like)));
|
|
40
|
+
}
|
|
41
|
+
const where = and(...filters);
|
|
42
|
+
const [totals] = await this.db.select({ total: sql`count(*)` }).from(orgUsers).innerJoin(users, eq(users.id, orgUsers.userId)).where(where);
|
|
43
|
+
return {
|
|
44
|
+
rows: (await this.db.select({
|
|
45
|
+
...orgUserProfileColumns,
|
|
46
|
+
status: orgUsers.status,
|
|
47
|
+
createdAt: orgUsers.createdAt,
|
|
48
|
+
entitlementCount: entitlementCountExpr,
|
|
49
|
+
avgProgress: avgProgressExpr
|
|
50
|
+
}).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.email, users.firstName, users.lastName, user.image).orderBy(...this.resolveOrder(query.sort)).limit(query.pageSize).offset((query.page - 1) * query.pageSize)).map(toStudent),
|
|
51
|
+
total: Number(totals?.total ?? 0),
|
|
52
|
+
page: query.page,
|
|
53
|
+
pageSize: query.pageSize
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async findById(orgId, id) {
|
|
57
|
+
const [row] = await this.db.select({
|
|
58
|
+
...orgUserProfileColumns,
|
|
59
|
+
status: orgUsers.status,
|
|
60
|
+
createdAt: orgUsers.createdAt,
|
|
61
|
+
entitlementCount: entitlementCountExpr,
|
|
62
|
+
avgProgress: avgProgressExpr
|
|
63
|
+
}).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.email, users.firstName, users.lastName, user.image).limit(1);
|
|
64
|
+
return row ? toStudent(row) : null;
|
|
65
|
+
}
|
|
66
|
+
resolveOrder(sort) {
|
|
67
|
+
const descending = sort?.startsWith("-") ?? false;
|
|
68
|
+
const field = sort ? descending ? sort.slice(1) : sort : "firstName";
|
|
69
|
+
const dir = descending ? desc : asc;
|
|
70
|
+
switch (field) {
|
|
71
|
+
case "email": return [dir(users.email)];
|
|
72
|
+
case "entitlementCount": return [dir(entitlementCountExpr)];
|
|
73
|
+
case "avgProgress": return [dir(avgProgressExpr)];
|
|
74
|
+
case "joinedAt": return [dir(orgUsers.createdAt)];
|
|
75
|
+
case "lastName": return [dir(users.lastName), dir(users.firstName)];
|
|
76
|
+
default: return [dir(users.firstName), dir(users.lastName)];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { DrizzleStudentsRepository };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=students.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"students.js","names":[],"sources":["../../src/repositories/students.ts"],"sourcesContent":["// students — Drizzle repository (implements the core outbound port).\n// A read-model over org users + entitlements: a \"student\" row in the report is\n// any org_users row with role 'student', with or without entitlements. Rooted at\n// `org_users` and scoped by its org, with entitlements LEFT JOINed in for the\n// aggregated count. Name/email come from the identity `users` row (INNER, since\n// user_id is NOT NULL); the avatar comes from the better-auth `user` table,\n// LEFT JOINed via that same identity row.\nimport { and, asc, desc, eq, ilike, or, sql, type SQL } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n Page,\n Student,\n StudentsQuery,\n StudentsReportRepository,\n} from '@headless-lms/core/reporting/students';\nimport { orgUsers, users, entitlements } from '../schema/index.js';\nimport { STUDENT_ROLE } from '@headless-lms/core/organizations';\nimport { user } from '../schema/better-auth.js';\nimport type { OrgUserStatus } from '@headless-lms/core/types';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { orgUserProfileColumns } from './org-user-profile.js';\n\nconst entitlementCountExpr = sql<number>`count(${entitlements.id})`;\n// Completion now lives in the progress domain; the students report no longer\n// derives a percentage from entitlements. Placeholder until wired to progress.\nconst avgProgressExpr = sql<number>`0`;\n\ninterface StudentRow {\n id: string;\n email: string;\n image: string | null;\n firstName: string | null;\n lastName: string | null;\n status: OrgUserStatus;\n createdAt: Date;\n entitlementCount: number;\n avgProgress: number;\n}\n\nfunction toStudent(row: StudentRow): Student {\n return {\n id: row.id,\n email: row.email,\n image: row.image ?? null,\n firstName: row.firstName,\n lastName: row.lastName,\n status: row.status,\n entitlementCount: Number(row.entitlementCount),\n avgProgress: Number(row.avgProgress),\n joinedAt: row.createdAt.toISOString(),\n lastActiveAt: null,\n };\n}\n\nexport class DrizzleStudentsRepository implements StudentsReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async list(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n // Staff share this table now — the role filter is what keeps them out of\n // the students report.\n const filters: SQL[] = [eq(orgUsers.orgId, orgId), eq(orgUsers.role, STUDENT_ROLE)];\n const q = query.search?.trim();\n if (q) {\n const like = `%${q}%`;\n filters.push(\n or(\n ilike(users.firstName, like),\n ilike(users.lastName, like),\n ilike(users.email, like),\n )!,\n );\n }\n const where = and(...filters);\n\n const [totals] = await this.db\n .select({ total: sql<number>`count(*)` })\n .from(orgUsers)\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .where(where);\n\n const rows = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(where)\n // Group by the full composite PK (orgId, id): grouping by id alone gives\n // Postgres no functional dependency for the other students columns.\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .orderBy(...this.resolveOrder(query.sort))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n return {\n rows: rows.map(toStudent),\n total: Number(totals?.total ?? 0),\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Student | null> {\n const [row] = await this.db\n .select({\n ...orgUserProfileColumns,\n status: orgUsers.status,\n createdAt: orgUsers.createdAt,\n entitlementCount: entitlementCountExpr,\n avgProgress: avgProgressExpr,\n })\n .from(orgUsers)\n .leftJoin(\n entitlements,\n and(eq(entitlements.orgId, orgUsers.orgId), eq(entitlements.orgUserId, orgUsers.id)),\n )\n .innerJoin(users, eq(users.id, orgUsers.userId))\n .leftJoin(user, eq(user.id, users.externalId))\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id), eq(orgUsers.role, STUDENT_ROLE)))\n .groupBy(\n orgUsers.orgId,\n orgUsers.id,\n orgUsers.status,\n users.email,\n users.firstName,\n users.lastName,\n user.image,\n )\n .limit(1);\n return row ? toStudent(row) : null;\n }\n\n private resolveOrder(sort?: string): SQL[] {\n const descending = sort?.startsWith('-') ?? false;\n const field = sort ? (descending ? sort.slice(1) : sort) : 'firstName';\n const dir = descending ? desc : asc;\n switch (field) {\n case 'email':\n return [dir(users.email)];\n case 'entitlementCount':\n return [dir(entitlementCountExpr)];\n case 'avgProgress':\n return [dir(avgProgressExpr)];\n case 'joinedAt':\n return [dir(orgUsers.createdAt)];\n case 'lastName':\n return [dir(users.lastName), dir(users.firstName)];\n case 'firstName':\n default:\n return [dir(users.firstName), dir(users.lastName)];\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//#region src/schema/assets.d.ts
|
|
2
|
+
declare const assets: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3
|
+
name: "assets";
|
|
4
|
+
schema: undefined;
|
|
5
|
+
columns: {
|
|
6
|
+
orgId: import("drizzle-orm/pg-core").PgColumn<{
|
|
7
|
+
name: "org_id";
|
|
8
|
+
tableName: "assets";
|
|
9
|
+
dataType: "string";
|
|
10
|
+
columnType: "PgText";
|
|
11
|
+
data: string;
|
|
12
|
+
driverParam: string;
|
|
13
|
+
notNull: true;
|
|
14
|
+
hasDefault: false;
|
|
15
|
+
isPrimaryKey: false;
|
|
16
|
+
isAutoincrement: false;
|
|
17
|
+
hasRuntimeDefault: false;
|
|
18
|
+
enumValues: [string, ...string[]];
|
|
19
|
+
baseColumn: never;
|
|
20
|
+
identity: undefined;
|
|
21
|
+
generated: undefined;
|
|
22
|
+
}, {}, {}>;
|
|
23
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
24
|
+
name: "id";
|
|
25
|
+
tableName: "assets";
|
|
26
|
+
dataType: "string";
|
|
27
|
+
columnType: "PgText";
|
|
28
|
+
data: string;
|
|
29
|
+
driverParam: string;
|
|
30
|
+
notNull: true;
|
|
31
|
+
hasDefault: true;
|
|
32
|
+
isPrimaryKey: false;
|
|
33
|
+
isAutoincrement: false;
|
|
34
|
+
hasRuntimeDefault: true;
|
|
35
|
+
enumValues: [string, ...string[]];
|
|
36
|
+
baseColumn: never;
|
|
37
|
+
identity: undefined;
|
|
38
|
+
generated: undefined;
|
|
39
|
+
}, {}, {}>;
|
|
40
|
+
key: import("drizzle-orm/pg-core").PgColumn<{
|
|
41
|
+
name: "key";
|
|
42
|
+
tableName: "assets";
|
|
43
|
+
dataType: "string";
|
|
44
|
+
columnType: "PgText";
|
|
45
|
+
data: string;
|
|
46
|
+
driverParam: string;
|
|
47
|
+
notNull: true;
|
|
48
|
+
hasDefault: false;
|
|
49
|
+
isPrimaryKey: false;
|
|
50
|
+
isAutoincrement: false;
|
|
51
|
+
hasRuntimeDefault: false;
|
|
52
|
+
enumValues: [string, ...string[]];
|
|
53
|
+
baseColumn: never;
|
|
54
|
+
identity: undefined;
|
|
55
|
+
generated: undefined;
|
|
56
|
+
}, {}, {}>;
|
|
57
|
+
kind: import("drizzle-orm/pg-core").PgColumn<{
|
|
58
|
+
name: "kind";
|
|
59
|
+
tableName: "assets";
|
|
60
|
+
dataType: "string";
|
|
61
|
+
columnType: "PgText";
|
|
62
|
+
data: "download" | "video" | "content";
|
|
63
|
+
driverParam: string;
|
|
64
|
+
notNull: true;
|
|
65
|
+
hasDefault: false;
|
|
66
|
+
isPrimaryKey: false;
|
|
67
|
+
isAutoincrement: false;
|
|
68
|
+
hasRuntimeDefault: false;
|
|
69
|
+
enumValues: ["video", "download", "content"];
|
|
70
|
+
baseColumn: never;
|
|
71
|
+
identity: undefined;
|
|
72
|
+
generated: undefined;
|
|
73
|
+
}, {}, {}>;
|
|
74
|
+
filename: import("drizzle-orm/pg-core").PgColumn<{
|
|
75
|
+
name: "filename";
|
|
76
|
+
tableName: "assets";
|
|
77
|
+
dataType: "string";
|
|
78
|
+
columnType: "PgText";
|
|
79
|
+
data: string;
|
|
80
|
+
driverParam: string;
|
|
81
|
+
notNull: true;
|
|
82
|
+
hasDefault: false;
|
|
83
|
+
isPrimaryKey: false;
|
|
84
|
+
isAutoincrement: false;
|
|
85
|
+
hasRuntimeDefault: false;
|
|
86
|
+
enumValues: [string, ...string[]];
|
|
87
|
+
baseColumn: never;
|
|
88
|
+
identity: undefined;
|
|
89
|
+
generated: undefined;
|
|
90
|
+
}, {}, {}>;
|
|
91
|
+
contentType: import("drizzle-orm/pg-core").PgColumn<{
|
|
92
|
+
name: "content_type";
|
|
93
|
+
tableName: "assets";
|
|
94
|
+
dataType: "string";
|
|
95
|
+
columnType: "PgText";
|
|
96
|
+
data: string;
|
|
97
|
+
driverParam: string;
|
|
98
|
+
notNull: true;
|
|
99
|
+
hasDefault: false;
|
|
100
|
+
isPrimaryKey: false;
|
|
101
|
+
isAutoincrement: false;
|
|
102
|
+
hasRuntimeDefault: false;
|
|
103
|
+
enumValues: [string, ...string[]];
|
|
104
|
+
baseColumn: never;
|
|
105
|
+
identity: undefined;
|
|
106
|
+
generated: undefined;
|
|
107
|
+
}, {}, {}>;
|
|
108
|
+
size: import("drizzle-orm/pg-core").PgColumn<{
|
|
109
|
+
name: "size";
|
|
110
|
+
tableName: "assets";
|
|
111
|
+
dataType: "number";
|
|
112
|
+
columnType: "PgBigInt53";
|
|
113
|
+
data: number;
|
|
114
|
+
driverParam: string | number;
|
|
115
|
+
notNull: true;
|
|
116
|
+
hasDefault: true;
|
|
117
|
+
isPrimaryKey: false;
|
|
118
|
+
isAutoincrement: false;
|
|
119
|
+
hasRuntimeDefault: false;
|
|
120
|
+
enumValues: undefined;
|
|
121
|
+
baseColumn: never;
|
|
122
|
+
identity: undefined;
|
|
123
|
+
generated: undefined;
|
|
124
|
+
}, {}, {}>;
|
|
125
|
+
status: import("drizzle-orm/pg-core").PgColumn<{
|
|
126
|
+
name: "status";
|
|
127
|
+
tableName: "assets";
|
|
128
|
+
dataType: "string";
|
|
129
|
+
columnType: "PgText";
|
|
130
|
+
data: "pending" | "ready";
|
|
131
|
+
driverParam: string;
|
|
132
|
+
notNull: true;
|
|
133
|
+
hasDefault: true;
|
|
134
|
+
isPrimaryKey: false;
|
|
135
|
+
isAutoincrement: false;
|
|
136
|
+
hasRuntimeDefault: false;
|
|
137
|
+
enumValues: ["pending", "ready"];
|
|
138
|
+
baseColumn: never;
|
|
139
|
+
identity: undefined;
|
|
140
|
+
generated: undefined;
|
|
141
|
+
}, {}, {}>;
|
|
142
|
+
uploadedBy: import("drizzle-orm/pg-core").PgColumn<{
|
|
143
|
+
name: "uploaded_by";
|
|
144
|
+
tableName: "assets";
|
|
145
|
+
dataType: "string";
|
|
146
|
+
columnType: "PgText";
|
|
147
|
+
data: string;
|
|
148
|
+
driverParam: string;
|
|
149
|
+
notNull: true;
|
|
150
|
+
hasDefault: false;
|
|
151
|
+
isPrimaryKey: false;
|
|
152
|
+
isAutoincrement: false;
|
|
153
|
+
hasRuntimeDefault: false;
|
|
154
|
+
enumValues: [string, ...string[]];
|
|
155
|
+
baseColumn: never;
|
|
156
|
+
identity: undefined;
|
|
157
|
+
generated: undefined;
|
|
158
|
+
}, {}, {}>;
|
|
159
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
160
|
+
name: "created_at";
|
|
161
|
+
tableName: "assets";
|
|
162
|
+
dataType: "date";
|
|
163
|
+
columnType: "PgTimestamp";
|
|
164
|
+
data: Date;
|
|
165
|
+
driverParam: string;
|
|
166
|
+
notNull: true;
|
|
167
|
+
hasDefault: true;
|
|
168
|
+
isPrimaryKey: false;
|
|
169
|
+
isAutoincrement: false;
|
|
170
|
+
hasRuntimeDefault: false;
|
|
171
|
+
enumValues: undefined;
|
|
172
|
+
baseColumn: never;
|
|
173
|
+
identity: undefined;
|
|
174
|
+
generated: undefined;
|
|
175
|
+
}, {}, {}>;
|
|
176
|
+
};
|
|
177
|
+
dialect: "pg";
|
|
178
|
+
}>;
|
|
179
|
+
//#endregion
|
|
180
|
+
export { assets };
|
|
181
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { organizations } from "./organizations.js";
|
|
2
|
+
import { bigint, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
|
|
3
|
+
import { genId } from "@headless-lms/core/shared/id";
|
|
4
|
+
//#region src/schema/assets.ts
|
|
5
|
+
const assets = pgTable("assets", {
|
|
6
|
+
orgId: text("org_id").notNull().references(() => organizations.id),
|
|
7
|
+
id: text("id").notNull().$defaultFn(() => genId("asset")),
|
|
8
|
+
key: text("key").notNull(),
|
|
9
|
+
kind: text("kind", { enum: [
|
|
10
|
+
"video",
|
|
11
|
+
"download",
|
|
12
|
+
"content"
|
|
13
|
+
] }).notNull(),
|
|
14
|
+
filename: text("filename").notNull(),
|
|
15
|
+
contentType: text("content_type").notNull(),
|
|
16
|
+
size: bigint("size", { mode: "number" }).notNull().default(0),
|
|
17
|
+
status: text("status", { enum: ["pending", "ready"] }).notNull().default("pending"),
|
|
18
|
+
uploadedBy: text("uploaded_by").notNull(),
|
|
19
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
20
|
+
}, (t) => ({ pk: primaryKey({ columns: [t.orgId, t.id] }) }));
|
|
21
|
+
//#endregion
|
|
22
|
+
export { assets };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.js","names":[],"sources":["../../src/schema/assets.ts"],"sourcesContent":["// assets table — the org's media library. Org-scoped: composite (org_id, id) PK\n// with org_id → organizations.id, mirroring the multi-tenant table shape.\nimport { pgTable, text, bigint, timestamp, primaryKey } from 'drizzle-orm/pg-core';\nimport type { Asset } from '@headless-lms/core/schemas';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { organizations } from './organizations.js';\nimport type { Expect, NoDrift } from './drift.js';\n\nexport const assets = pgTable(\n 'assets',\n {\n orgId: text('org_id')\n .notNull()\n .references(() => organizations.id),\n id: text('id')\n .notNull()\n .$defaultFn(() => genId('asset')),\n key: text('key').notNull(),\n kind: text('kind', { enum: ['video', 'download', 'content'] }).notNull(),\n filename: text('filename').notNull(),\n contentType: text('content_type').notNull(),\n // Bytes; bigint because media files exceed int4. 0 until upload confirmed.\n size: bigint('size', { mode: 'number' }).notNull().default(0),\n status: text('status', { enum: ['pending', 'ready'] })\n .notNull()\n .default('pending'),\n uploadedBy: text('uploaded_by').notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (t) => ({ pk: primaryKey({ columns: [t.orgId, t.id] }) }),\n);\n\ntype _AssetsDrift = Expect<NoDrift<typeof assets.$inferSelect, Asset>>;\n"],"mappings":";;;;AAQA,MAAa,SAAS,QACpB,UACA;CACE,OAAO,KAAK,QAAQ,CAAC,CAClB,QAAQ,CAAC,CACT,iBAAiB,cAAc,EAAE;CACpC,IAAI,KAAK,IAAI,CAAC,CACX,QAAQ,CAAC,CACT,iBAAiB,MAAM,OAAO,CAAC;CAClC,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ;CACzB,MAAM,KAAK,QAAQ,EAAE,MAAM;EAAC;EAAS;EAAY;CAAS,EAAE,CAAC,CAAC,CAAC,QAAQ;CACvE,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ;CACnC,aAAa,KAAK,cAAc,CAAC,CAAC,QAAQ;CAE1C,MAAM,OAAO,QAAQ,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC,CACnD,QAAQ,CAAC,CACT,QAAQ,SAAS;CACpB,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;CACxC,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAClF,IACC,OAAO,EAAE,IAAI,WAAW,EAAE,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EACzD"}
|