@headless-lms/adapter-db 0.1.0 → 0.2.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 +2 -1
- package/dist/index.js +3 -2
- package/dist/migrate.js.map +1 -1
- package/dist/repositories/assets.d.ts +1 -1
- package/dist/repositories/assets.js +4 -1
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +7 -5
- package/dist/repositories/automations.js +35 -10
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/content.d.ts +21 -7
- package/dist/repositories/content.js +168 -18
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/course-analytics.d.ts +15 -0
- package/dist/repositories/course-analytics.js +137 -0
- package/dist/repositories/course-analytics.js.map +1 -0
- package/dist/repositories/credentials.js +2 -0
- package/dist/repositories/credentials.js.map +1 -1
- package/dist/repositories/dashboard.js +2 -0
- package/dist/repositories/dashboard.js.map +1 -1
- package/dist/repositories/discussion.d.ts +1 -1
- package/dist/repositories/discussion.js +2 -0
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +11 -5
- package/dist/repositories/entitlements.js +42 -17
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/identity.js +2 -0
- 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/learn.js +6 -4
- package/dist/repositories/learn.js.map +1 -1
- package/dist/repositories/members.js +2 -0
- package/dist/repositories/members.js.map +1 -1
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +4 -6
- package/dist/repositories/organizations.js +26 -45
- 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 +97 -8
- package/dist/repositories/pg-errors.js.map +1 -1
- 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/repositories/students.d.ts +2 -1
- package/dist/repositories/students.js +80 -3
- package/dist/repositories/students.js.map +1 -1
- package/dist/schema/assets.d.ts +17 -0
- package/dist/schema/assets.js +2 -1
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +85 -8
- package/dist/schema/automations.js +7 -4
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/better-auth.js.map +1 -1
- package/dist/schema/content.d.ts +272 -22
- package/dist/schema/content.js +40 -5
- 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 +34 -0
- package/dist/schema/discussion.js +4 -2
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/entitlements.d.ts +52 -1
- package/dist/schema/entitlements.js +15 -5
- package/dist/schema/entitlements.js.map +1 -1
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +3 -3
- package/dist/schema/index.js +4 -2
- 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 +17 -0
- package/dist/schema/organizations.js +2 -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/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/_journal.json +16 -2
- package/package.json +3 -2
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
|
|
2
|
-
import {
|
|
1
|
+
import { activities, activityAssets, bundleItems, bundles, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
|
|
2
|
+
import { settings } from "../schema/settings.js";
|
|
3
|
+
import { isUniqueViolation, translateDbErrors } from "./pg-errors.js";
|
|
4
|
+
import { deepMerge } from "./settings.js";
|
|
3
5
|
import { and, asc, count, desc, eq, ilike, inArray, or, sql } from "drizzle-orm";
|
|
4
6
|
import { genId } from "@headless-lms/core/shared/id";
|
|
5
|
-
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
7
|
import { ConflictError, NotFoundError } from "@headless-lms/core/shared/errors";
|
|
8
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
9
|
+
import { courseSettingsDefaults } from "@headless-lms/core/schemas";
|
|
7
10
|
//#region src/repositories/content.ts
|
|
8
|
-
|
|
11
|
+
const SETTINGS_NAMESPACE = "content";
|
|
12
|
+
const courseSettingsJoin = and(eq(settings.orgId, courses.orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, courses.id));
|
|
13
|
+
function toCourse(row, storedSettings) {
|
|
9
14
|
return {
|
|
10
15
|
...row,
|
|
11
|
-
status: row.status
|
|
16
|
+
status: row.status,
|
|
17
|
+
settings: {
|
|
18
|
+
...courseSettingsDefaults,
|
|
19
|
+
...storedSettings
|
|
20
|
+
}
|
|
12
21
|
};
|
|
13
22
|
}
|
|
14
23
|
const sortColumns = {
|
|
@@ -33,6 +42,11 @@ function toDownload(row) {
|
|
|
33
42
|
status: row.status
|
|
34
43
|
};
|
|
35
44
|
}
|
|
45
|
+
const bundleSortColumns = {
|
|
46
|
+
name: bundles.name,
|
|
47
|
+
createdAt: bundles.createdAt,
|
|
48
|
+
updatedAt: bundles.updatedAt
|
|
49
|
+
};
|
|
36
50
|
var DrizzleContentRepository = class {
|
|
37
51
|
db;
|
|
38
52
|
logger;
|
|
@@ -45,7 +59,7 @@ var DrizzleContentRepository = class {
|
|
|
45
59
|
tx(fn) {
|
|
46
60
|
return this.db.transaction(fn);
|
|
47
61
|
}
|
|
48
|
-
async
|
|
62
|
+
async listCourses(orgId, query) {
|
|
49
63
|
const conditions = [eq(courses.orgId, orgId)];
|
|
50
64
|
if (query.status) conditions.push(eq(courses.status, query.status));
|
|
51
65
|
if (query.category) conditions.push(eq(courses.category, query.category));
|
|
@@ -68,20 +82,26 @@ var DrizzleContentRepository = class {
|
|
|
68
82
|
}
|
|
69
83
|
const sortExpr = sortColumns[sortKey] ?? courses.createdAt;
|
|
70
84
|
const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
|
|
71
|
-
const rows = await this.db.select(
|
|
85
|
+
const rows = await this.db.select({
|
|
86
|
+
course: courses,
|
|
87
|
+
settingsValue: settings.value
|
|
88
|
+
}).from(courses).leftJoin(settings, courseSettingsJoin).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
72
89
|
const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);
|
|
73
90
|
return {
|
|
74
|
-
rows: rows.map(toCourse),
|
|
91
|
+
rows: rows.map((row) => toCourse(row.course, row.settingsValue)),
|
|
75
92
|
total: totalRow?.value ?? 0,
|
|
76
93
|
page: query.page,
|
|
77
94
|
pageSize: query.pageSize
|
|
78
95
|
};
|
|
79
96
|
}
|
|
80
|
-
async
|
|
81
|
-
const [row] = await this.db.select(
|
|
82
|
-
|
|
97
|
+
async findCourseById(orgId, id) {
|
|
98
|
+
const [row] = await this.db.select({
|
|
99
|
+
course: courses,
|
|
100
|
+
settingsValue: settings.value
|
|
101
|
+
}).from(courses).leftJoin(settings, courseSettingsJoin).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).limit(1);
|
|
102
|
+
return row ? toCourse(row.course, row.settingsValue) : null;
|
|
83
103
|
}
|
|
84
|
-
async
|
|
104
|
+
async createCourse(orgId, input, slug) {
|
|
85
105
|
const id = genId("course");
|
|
86
106
|
await this.db.insert(contentItems).values({
|
|
87
107
|
orgId,
|
|
@@ -97,31 +117,68 @@ var DrizzleContentRepository = class {
|
|
|
97
117
|
category: input.category ?? ""
|
|
98
118
|
}).returning({ id: courses.id });
|
|
99
119
|
if (!inserted) throw new Error("failed to insert course");
|
|
100
|
-
const created = await this.
|
|
120
|
+
const created = await this.findCourseById(orgId, inserted.id);
|
|
101
121
|
if (!created) throw new Error("failed to load created course");
|
|
102
122
|
return created;
|
|
103
123
|
}
|
|
104
|
-
async
|
|
124
|
+
async updateCourse(orgId, id, patch) {
|
|
105
125
|
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
106
126
|
if (patch.title !== void 0) set.title = patch.title;
|
|
107
127
|
if (patch.description !== void 0) set.description = patch.description;
|
|
108
128
|
if (patch.category !== void 0) set.category = patch.category;
|
|
109
129
|
if (patch.status !== void 0) set.status = patch.status;
|
|
110
130
|
if (patch.thumbnailAssetId !== void 0) set.thumbnailAssetId = patch.thumbnailAssetId;
|
|
111
|
-
if (patch.settings !== void 0) set.settings = sql`${courses.settings} || ${JSON.stringify(patch.settings)}::jsonb`;
|
|
112
131
|
const [updated] = await this.db.update(courses).set(set).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).returning({ id: courses.id });
|
|
113
132
|
if (!updated) return null;
|
|
114
|
-
return this.
|
|
133
|
+
return this.findCourseById(orgId, id);
|
|
134
|
+
}
|
|
135
|
+
async patchCourseSettings(orgId, id, value) {
|
|
136
|
+
const key = and(eq(settings.orgId, orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, id));
|
|
137
|
+
const merged = await this.tx(async (tx) => {
|
|
138
|
+
let [existing] = await tx.select().from(settings).where(key).for("update");
|
|
139
|
+
if (!existing) {
|
|
140
|
+
const [inserted] = await tx.insert(settings).values({
|
|
141
|
+
orgId,
|
|
142
|
+
namespace: SETTINGS_NAMESPACE,
|
|
143
|
+
scopeId: id,
|
|
144
|
+
value
|
|
145
|
+
}).onConflictDoNothing().returning();
|
|
146
|
+
if (inserted) return inserted.value;
|
|
147
|
+
[existing] = await tx.select().from(settings).where(key).for("update");
|
|
148
|
+
if (!existing) throw new Error("course settings patch: row vanished mid-transaction");
|
|
149
|
+
}
|
|
150
|
+
const [updated] = await tx.update(settings).set({
|
|
151
|
+
value: deepMerge(existing.value, value),
|
|
152
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
153
|
+
}).where(key).returning();
|
|
154
|
+
if (!updated) throw new Error("course settings patch returned no row");
|
|
155
|
+
return updated.value;
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
...courseSettingsDefaults,
|
|
159
|
+
...merged
|
|
160
|
+
};
|
|
115
161
|
}
|
|
116
|
-
async
|
|
162
|
+
async deleteCourse(orgId, id) {
|
|
117
163
|
return (await this.db.delete(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, "course"))).returning({ id: contentItems.id })).length > 0;
|
|
118
164
|
}
|
|
119
|
-
|
|
165
|
+
listCourseModules(orgId, courseId) {
|
|
120
166
|
return this.tx((tx) => this.listModules(tx, orgId, courseId));
|
|
121
167
|
}
|
|
122
168
|
async listActivitiesForCourse(orgId, courseId) {
|
|
123
169
|
return this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activities.moduleId, activities.seq);
|
|
124
170
|
}
|
|
171
|
+
async listActivityAssetsForCourse(orgId, courseId) {
|
|
172
|
+
return this.db.select({
|
|
173
|
+
orgId: activityAssets.orgId,
|
|
174
|
+
id: activityAssets.id,
|
|
175
|
+
activityId: activityAssets.activityId,
|
|
176
|
+
assetId: activityAssets.assetId,
|
|
177
|
+
seq: activityAssets.seq,
|
|
178
|
+
createdAt: activityAssets.createdAt,
|
|
179
|
+
updatedAt: activityAssets.updatedAt
|
|
180
|
+
}).from(activityAssets).innerJoin(activities, and(eq(activities.orgId, activityAssets.orgId), eq(activities.id, activityAssets.activityId))).where(and(eq(activityAssets.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activityAssets.activityId, activityAssets.seq);
|
|
181
|
+
}
|
|
125
182
|
async findActivity(orgId, activityId) {
|
|
126
183
|
const [row] = await this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId))).limit(1);
|
|
127
184
|
return row ?? null;
|
|
@@ -343,7 +400,100 @@ var DrizzleContentRepository = class {
|
|
|
343
400
|
for (const [seq, assetId] of assetIds.entries()) await this.db.update(downloadAssets).set({ seq }).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId), eq(downloadAssets.assetId, assetId)));
|
|
344
401
|
return this.listDownloadAssets(orgId, downloadId);
|
|
345
402
|
}
|
|
403
|
+
async listBundles(orgId, query) {
|
|
404
|
+
const conditions = [eq(bundles.orgId, orgId)];
|
|
405
|
+
const search = query.search?.trim();
|
|
406
|
+
if (search) conditions.push(ilike(bundles.name, `%${search}%`));
|
|
407
|
+
const where = and(...conditions);
|
|
408
|
+
let sortKey = "createdAt";
|
|
409
|
+
let direction = "desc";
|
|
410
|
+
if (query.sort) {
|
|
411
|
+
const isDesc = query.sort.startsWith("-");
|
|
412
|
+
const key = isDesc ? query.sort.slice(1) : query.sort;
|
|
413
|
+
if (key in bundleSortColumns) {
|
|
414
|
+
sortKey = key;
|
|
415
|
+
direction = isDesc ? "desc" : "asc";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const sortExpr = bundleSortColumns[sortKey] ?? bundles.createdAt;
|
|
419
|
+
const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
|
|
420
|
+
const rows = await this.db.select().from(bundles).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
421
|
+
const [totalRow] = await this.db.select({ value: count() }).from(bundles).where(where);
|
|
422
|
+
return {
|
|
423
|
+
rows,
|
|
424
|
+
total: totalRow?.value ?? 0,
|
|
425
|
+
page: query.page,
|
|
426
|
+
pageSize: query.pageSize
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async findBundleById(orgId, id) {
|
|
430
|
+
const [row] = await this.db.select().from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).limit(1);
|
|
431
|
+
return row ?? null;
|
|
432
|
+
}
|
|
433
|
+
/** The bundle and its initial items are one write: an unknown content id
|
|
434
|
+
* rolls the bundle back rather than leaving it created and half-filled. */
|
|
435
|
+
async createBundle(orgId, input) {
|
|
436
|
+
const contentIds = [...new Set(input.contentIds ?? [])];
|
|
437
|
+
return this.tx(async (tx) => {
|
|
438
|
+
const [row] = await tx.insert(bundles).values({
|
|
439
|
+
orgId,
|
|
440
|
+
id: genId("bundle"),
|
|
441
|
+
name: input.name
|
|
442
|
+
}).returning();
|
|
443
|
+
if (!row) throw new Error("failed to load created bundle");
|
|
444
|
+
if (contentIds.length > 0) {
|
|
445
|
+
const found = await tx.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), inArray(contentItems.id, contentIds)));
|
|
446
|
+
const known = new Set(found.map((item) => item.id));
|
|
447
|
+
const missing = contentIds.find((id) => !known.has(id));
|
|
448
|
+
if (missing) throw new NotFoundError("Content", missing);
|
|
449
|
+
await tx.insert(bundleItems).values(contentIds.map((contentId) => ({
|
|
450
|
+
orgId,
|
|
451
|
+
bundleId: row.id,
|
|
452
|
+
contentId
|
|
453
|
+
})));
|
|
454
|
+
}
|
|
455
|
+
return row;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async updateBundle(orgId, id, patch) {
|
|
459
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
460
|
+
if (patch.name !== void 0) set.name = patch.name;
|
|
461
|
+
const [row] = await this.db.update(bundles).set(set).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning();
|
|
462
|
+
return row ?? null;
|
|
463
|
+
}
|
|
464
|
+
async deleteBundle(orgId, id) {
|
|
465
|
+
return (await this.db.delete(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning({ id: bundles.id })).length > 0;
|
|
466
|
+
}
|
|
467
|
+
async listBundleItems(orgId, bundleId) {
|
|
468
|
+
return this.db.select().from(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId))).orderBy(asc(bundleItems.createdAt));
|
|
469
|
+
}
|
|
470
|
+
/** Assert the bundle exists in the org; throw otherwise. */
|
|
471
|
+
async assertBundle(orgId, bundleId) {
|
|
472
|
+
const [row] = await this.db.select({ id: bundles.id }).from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, bundleId))).limit(1);
|
|
473
|
+
if (!row) throw new NotFoundError("Bundle", bundleId);
|
|
474
|
+
}
|
|
475
|
+
async addBundleItem(orgId, bundleId, contentId) {
|
|
476
|
+
await this.assertBundle(orgId, bundleId);
|
|
477
|
+
const [item] = await this.db.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
|
|
478
|
+
if (!item) throw new NotFoundError("Content", contentId);
|
|
479
|
+
await this.db.insert(bundleItems).values({
|
|
480
|
+
orgId,
|
|
481
|
+
bundleId,
|
|
482
|
+
contentId
|
|
483
|
+
}).onConflictDoNothing({ target: [
|
|
484
|
+
bundleItems.orgId,
|
|
485
|
+
bundleItems.bundleId,
|
|
486
|
+
bundleItems.contentId
|
|
487
|
+
] });
|
|
488
|
+
return this.listBundleItems(orgId, bundleId);
|
|
489
|
+
}
|
|
490
|
+
async removeBundleItem(orgId, bundleId, contentId) {
|
|
491
|
+
await this.assertBundle(orgId, bundleId);
|
|
492
|
+
await this.db.delete(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId), eq(bundleItems.contentId, contentId)));
|
|
493
|
+
return this.listBundleItems(orgId, bundleId);
|
|
494
|
+
}
|
|
346
495
|
};
|
|
496
|
+
translateDbErrors(DrizzleContentRepository);
|
|
347
497
|
//#endregion
|
|
348
498
|
export { DrizzleContentRepository };
|
|
349
499
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","names":[],"sources":["../../src/repositories/content.ts"],"sourcesContent":["// content — Drizzle repository for the course aggregate root (implements the core\n// outbound `ContentRepository` port). Org-scoped: every method takes the domain\n// `organizations.id` and constrains its queries to that tenant. Rows come back\n// as stored — derived counts and cross-entity composition live in reporting.\nimport {\n eq,\n and,\n sql,\n count,\n asc,\n desc,\n ilike,\n or,\n inArray,\n type SQL,\n type AnyColumn,\n} from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { DbExecutor, Tx } from '../client.js';\nimport type {\n Activity,\n AddDownloadAssetInput,\n ContentRepository,\n Course,\n CourseStatus,\n CreateCourseInput,\n CreateDownloadInput,\n Download,\n DownloadAsset,\n DownloadStatus,\n ListCoursesQuery,\n ListDownloadsQuery,\n Module,\n Page,\n SaveActivityInput,\n UpdateCourseInput,\n UpdateDownloadInput,\n} from '@headless-lms/core/content';\nimport {\n contentItems,\n courses,\n modules,\n activities,\n activityAssets,\n downloads,\n downloadAssets,\n} from '../schema/content.js';\nimport { genId } from '@headless-lms/core/shared/id';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { NotFoundError, ConflictError } from '@headless-lms/core/shared/errors';\nimport { isUniqueViolation } from './pg-errors.js';\n\nfunction toCourse(row: typeof courses.$inferSelect): Course {\n return { ...row, status: row.status as CourseStatus };\n}\n\n// Columns a caller may sort by. Default falls back to createdAt desc.\nconst sortColumns: Record<string, AnyColumn | SQL> = {\n title: courses.title,\n slug: courses.slug,\n status: courses.status,\n category: courses.category,\n createdAt: courses.createdAt,\n updatedAt: courses.updatedAt,\n};\n\nconst downloadSortColumns: Record<string, AnyColumn | SQL> = {\n title: downloads.title,\n slug: downloads.slug,\n status: downloads.status,\n category: downloads.category,\n createdAt: downloads.createdAt,\n updatedAt: downloads.updatedAt,\n};\n\nfunction toDownload(row: typeof downloads.$inferSelect): Download {\n return { ...row, status: row.status as DownloadStatus };\n}\n\nexport class DrizzleContentRepository implements ContentRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it\n * nests as a savepoint inside the surrounding transaction. */\n private tx<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {\n return (this.db as NodePgDatabase).transaction(fn);\n }\n\n async list(orgId: string, query: ListCoursesQuery): Promise<Page<Course>> {\n const conditions: SQL[] = [eq(courses.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(courses.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(courses.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(courses.title, like), ilike(courses.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = 'createdAt';\n let direction: 'asc' | 'desc' = 'desc';\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in sortColumns) {\n sortKey = key;\n direction = isDesc ? 'desc' : 'asc';\n }\n }\n const sortExpr = sortColumns[sortKey] ?? courses.createdAt;\n const orderBy = direction === 'desc' ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(courses)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);\n\n return {\n rows: rows.map(toCourse),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Course | null> {\n const [row] = await this.db\n .select()\n .from(courses)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .limit(1);\n return row ? toCourse(row) : null;\n }\n\n async create(orgId: string, input: CreateCourseInput, slug: string): Promise<Course> {\n // Registry row + concrete row share one id. Both inserts run on the same\n // executor — mutations reach this repository tx-bound (ContentUnitOfWork),\n // so they commit or roll back together.\n const id = genId('course');\n await this.db.insert(contentItems).values({ orgId, id, type: 'course' });\n const [inserted] = await this.db\n .insert(courses)\n .values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? '',\n category: input.category ?? '',\n })\n .returning({ id: courses.id });\n if (!inserted) {\n throw new Error('failed to insert course');\n }\n const created = await this.findById(orgId, inserted.id);\n if (!created) {\n throw new Error('failed to load created course');\n }\n return created;\n }\n\n async update(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n // null is meaningful here — it clears the cover — so only `undefined` skips.\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n if (patch.settings !== undefined) {\n // Shallow merge, so a partial patch can't drop the keys it omits.\n set.settings = sql`${courses.settings} || ${JSON.stringify(patch.settings)}::jsonb`;\n }\n\n const [updated] = await this.db\n .update(courses)\n .set(set)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .returning({ id: courses.id });\n if (!updated) {\n return null;\n }\n return this.findById(orgId, id);\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n // Deletes go through the registry: cascades to the course row and its\n // entitlements in one statement. Never delete from `courses` directly —\n // that would strand the registry row.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, 'course')),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n // --- modules & activities ----------------------------------------------\n // Every mutation returns the course's full ordered module list (by `seq`),\n // so `listModules` runs at the end of every mutation within the same\n // transaction. An Activity's media is the many-to-many `activity_assets`\n // join: `saveActivity` upserts the row and replaces its asset links; deletes\n // drop the links first (they FK the activity), then the activity rows.\n\n listForCourse(orgId: string, courseId: string): Promise<Module[]> {\n return this.tx((tx) => this.listModules(tx, orgId, courseId));\n }\n\n async listActivitiesForCourse(orgId: string, courseId: string): Promise<Activity[]> {\n return this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activities.moduleId, activities.seq);\n }\n\n async findActivity(orgId: string, activityId: string): Promise<Activity | null> {\n const [row] = await this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)))\n .limit(1);\n return row ?? null;\n }\n\n async findModule(orgId: string, moduleId: string): Promise<Module | null> {\n const [row] = await this.db\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId)))\n .limit(1);\n return row ?? null;\n }\n\n /** The course's full ordered module list. */\n private async listModules(tx: Tx, orgId: string, courseId: string): Promise<Module[]> {\n return tx\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)))\n .orderBy(modules.seq);\n }\n\n /** Assert the module belongs to the org + course; throw otherwise. */\n private async assertModule(\n tx: Tx,\n orgId: string,\n courseId: string,\n moduleId: string,\n ): Promise<void> {\n const [row] = await tx\n .select({ id: modules.id })\n .from(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n )\n .limit(1);\n if (!row) {\n throw new NotFoundError('Module', moduleId);\n }\n }\n\n /** Replace an activity's ordered asset links with `assetIds`. */\n private async replaceActivityAssets(\n tx: Tx,\n orgId: string,\n activityId: string,\n assetIds: string[],\n ): Promise<void> {\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n if (assetIds.length) {\n await tx\n .insert(activityAssets)\n .values(assetIds.map((assetId, i) => ({ orgId, activityId, assetId, seq: i })));\n }\n }\n\n createModule(orgId: string, courseId: string, title: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n const existing = await tx\n .select({ seq: modules.seq })\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n await tx.insert(modules).values({ orgId, courseId, title, seq: nextSeq });\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n updateModule(\n orgId: string,\n courseId: string,\n moduleId: string,\n title: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n await tx\n .update(modules)\n .set({ title })\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the module's activities: their asset links first (they FK the\n // activity), then the activity rows, then the module.\n const activityRows = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const activityIds = activityRows.map((a) => a.id);\n\n if (activityIds.length) {\n await tx\n .delete(activityAssets)\n .where(\n and(eq(activityAssets.orgId, orgId), inArray(activityAssets.activityId, activityIds)),\n );\n await tx\n .delete(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n }\n\n await tx\n .delete(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]> {\n return this.tx(async (tx) => {\n // Two-phase to dodge the unique(org_id, id) is fine, but modules have no\n // unique(seq); still, park then assign to stay consistent with activities.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(modules)\n .set({ seq: i })\n .where(\n and(\n eq(modules.orgId, orgId),\n eq(modules.courseId, courseId),\n eq(modules.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderActivities(\n orgId: string,\n courseId: string,\n moduleId: string,\n orderedIds: string[],\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n // Two-phase to dodge the unique(org_id, module_id, seq) constraint mid-swap:\n // park rows at negative seqs, then assign the final 0..n-1.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: -(i + 1) })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: i })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n saveActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n input: SaveActivityInput,\n activityId?: string,\n ): Promise<{ modules: Module[]; activity: Activity }> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n let savedId: string;\n if (activityId) {\n const [existing] = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n )\n .limit(1);\n if (!existing) {\n throw new NotFoundError('Activity', activityId);\n }\n\n await tx\n .update(activities)\n .set({ settings: input.settings ?? null })\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)));\n if (input.assetIds !== undefined) {\n await this.replaceActivityAssets(tx, orgId, activityId, input.assetIds);\n }\n savedId = activityId;\n } else {\n const existing = await tx\n .select({ seq: activities.seq })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n\n const [ins] = await tx\n .insert(activities)\n .values({ orgId, moduleId, courseId, seq: nextSeq, settings: input.settings ?? null })\n .returning({ id: activities.id });\n if (!ins) {\n throw new Error('failed to insert activity');\n }\n await this.replaceActivityAssets(tx, orgId, ins.id, input.assetIds ?? []);\n savedId = ins.id;\n }\n\n const [activity] = await tx\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, savedId)))\n .limit(1);\n if (!activity) {\n throw new Error('failed to load saved activity');\n }\n return { modules: await this.listModules(tx, orgId, courseId), activity };\n });\n }\n\n deleteActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n activityId: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the asset links first (they FK the activity), then the activity.\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n await tx\n .delete(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n );\n\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n // --- downloads --------------------------------------------------------\n\n async listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>> {\n const conditions: SQL[] = [eq(downloads.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(downloads.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(downloads.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(downloads.title, like), ilike(downloads.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = 'createdAt';\n let direction: 'asc' | 'desc' = 'desc';\n if (query.sort) {\n const isDesc = query.sort.startsWith('-');\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in downloadSortColumns) {\n sortKey = key;\n direction = isDesc ? 'desc' : 'asc';\n }\n }\n const sortExpr = downloadSortColumns[sortKey] ?? downloads.createdAt;\n const orderBy = direction === 'desc' ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(downloads)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(downloads).where(where);\n\n return {\n rows: rows.map(toDownload),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getDownload(orgId: string, id: string): Promise<Download | null> {\n const [row] = await this.db\n .select()\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .limit(1);\n return row ? toDownload(row) : null;\n }\n\n async createDownload(\n orgId: string,\n input: CreateDownloadInput,\n slug: string,\n ): Promise<Download> {\n // Registry row + concrete row share one id, same as courses. Both inserts\n // run on the same executor — mutations reach this repository tx-bound.\n const id = genId('download');\n await this.db.insert(contentItems).values({ orgId, id, type: 'download' });\n await this.db.insert(downloads).values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? '',\n category: input.category ?? '',\n });\n const created = await this.getDownload(orgId, id);\n if (!created) {\n throw new Error('failed to load created download');\n }\n return created;\n }\n\n async updateDownload(\n orgId: string,\n id: string,\n patch: UpdateDownloadInput,\n ): Promise<Download | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(downloads)\n .set(set)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .returning({ id: downloads.id });\n if (!updated) {\n return null;\n }\n return this.getDownload(orgId, id);\n }\n\n async deleteDownload(orgId: string, id: string): Promise<boolean> {\n // Through the registry, so the cascade reaches the download row, its asset\n // links, and the grants. Never delete from `downloads` directly.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, 'download'),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n async listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]> {\n return this.db\n .select()\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)))\n .orderBy(asc(downloadAssets.seq));\n }\n\n /** Assert the download exists in the org; throw otherwise. */\n private async assertDownload(orgId: string, downloadId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: downloads.id })\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, downloadId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError('Download', downloadId);\n }\n }\n\n async addDownloadAsset(\n orgId: string,\n downloadId: string,\n input: AddDownloadAssetInput,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n const [maxRow] = await this.db\n .select({ maxSeq: sql<number | null>`max(${downloadAssets.seq})` })\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)));\n const nextSeq = (maxRow?.maxSeq ?? -1) + 1;\n try {\n await this.db.insert(downloadAssets).values({\n orgId,\n downloadId,\n assetId: input.assetId,\n seq: nextSeq,\n displayName: input.displayName ?? null,\n });\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError('This asset is already linked to the download');\n }\n throw err;\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async removeDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .delete(downloadAssets)\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async renameDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n displayName: string | null,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .update(downloadAssets)\n .set({ displayName })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async reorderDownloadAssets(\n orgId: string,\n downloadId: string,\n assetIds: string[],\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n // The service has already verified this is exactly the current set.\n for (const [seq, assetId] of assetIds.entries()) {\n await this.db\n .update(downloadAssets)\n .set({ seq })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n}\n"],"mappings":";;;;;;;AAqDA,SAAS,SAAS,KAA0C;CAC1D,OAAO;EAAE,GAAG;EAAK,QAAQ,IAAI;CAAuB;AACtD;AAGA,MAAM,cAA+C;CACnD,OAAO,QAAQ;CACf,MAAM,QAAQ;CACd,QAAQ,QAAQ;CAChB,UAAU,QAAQ;CAClB,WAAW,QAAQ;CACnB,WAAW,QAAQ;AACrB;AAEA,MAAM,sBAAuD;CAC3D,OAAO,UAAU;CACjB,MAAM,UAAU;CAChB,QAAQ,UAAU;CAClB,UAAU,UAAU;CACpB,WAAW,UAAU;CACrB,WAAW,UAAU;AACvB;AAEA,SAAS,WAAW,KAA8C;CAChE,OAAO;EAAE,GAAG;EAAK,QAAQ,IAAI;CAAyB;AACxD;AAEA,IAAa,2BAAb,MAAmE;CAE9C;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;;;CAIH,GAAc,IAAwC;EACpD,OAAQ,KAAK,GAAsB,YAAY,EAAE;CACnD;CAEA,MAAM,KAAK,OAAe,OAAgD;EACxE,MAAM,aAAoB,CAAC,GAAG,QAAQ,OAAO,KAAK,CAAC;EACnD,IAAI,MAAM,QACR,WAAW,KAAK,GAAG,QAAQ,QAAQ,MAAM,MAAM,CAAC;EAElD,IAAI,MAAM,UACR,WAAW,KAAK,GAAG,QAAQ,UAAU,MAAM,QAAQ,CAAC;EAGtD,MAAM,SAAS,MAAM,QAAQ,KAAK;EAClC,IAAI,QAAQ;GACV,MAAM,OAAO,IAAI,OAAO;GACxB,MAAM,QAAQ,GAAG,MAAM,QAAQ,OAAO,IAAI,GAAG,MAAM,QAAQ,UAAU,IAAI,CAAC;GAC1E,IAAI,OACF,WAAW,KAAK,KAAK;EAEzB;EAEA,MAAM,QAAQ,IAAI,GAAG,UAAU;EAG/B,IAAI,UAAU;EACd,IAAI,YAA4B;EAChC,IAAI,MAAM,MAAM;GACd,MAAM,SAAS,MAAM,KAAK,WAAW,GAAG;GACxC,MAAM,MAAM,SAAS,MAAM,KAAK,MAAM,CAAC,IAAI,MAAM;GACjD,IAAI,OAAO,aAAa;IACtB,UAAU;IACV,YAAY,SAAS,SAAS;GAChC;EACF;EACA,MAAM,WAAW,YAAY,YAAY,QAAQ;EACjD,MAAM,UAAU,cAAc,SAAS,KAAK,QAAQ,IAAI,IAAI,QAAQ;EAEpE,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MAAM,KAAK,CAAC,CACZ,QAAQ,OAAO,CAAC,CAChB,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ;EAE3C,MAAM,CAAC,YAAY,MAAM,KAAK,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,KAAK;EAErF,OAAO;GACL,MAAM,KAAK,IAAI,QAAQ;GACvB,OAAO,UAAU,SAAS;GAC1B,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;CAEA,MAAM,SAAS,OAAe,IAAoC;EAChE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CACxD,MAAM,CAAC;EACV,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,MAAM,OAAO,OAAe,OAA0B,MAA+B;EAInF,MAAM,KAAK,MAAM,QAAQ;EACzB,MAAM,KAAK,GAAG,OAAO,YAAY,CAAC,CAAC,OAAO;GAAE;GAAO;GAAI,MAAM;EAAS,CAAC;EACvE,MAAM,CAAC,YAAY,MAAM,KAAK,GAC3B,OAAO,OAAO,CAAC,CACf,OAAO;GACN;GACA;GACA,OAAO,MAAM;GACb;GACA,aAAa,MAAM,eAAe;GAClC,UAAU,MAAM,YAAY;EAC9B,CAAC,CAAC,CACD,UAAU,EAAE,IAAI,QAAQ,GAAG,CAAC;EAC/B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,yBAAyB;EAE3C,MAAM,UAAU,MAAM,KAAK,SAAS,OAAO,SAAS,EAAE;EACtD,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,+BAA+B;EAEjD,OAAO;CACT;CAEA,MAAM,OAAO,OAAe,IAAY,OAAkD;EACxF,MAAM,MAA+B,EAAE,2BAAW,IAAI,KAAK,EAAE;EAC7D,IAAI,MAAM,UAAU,KAAA,GAClB,IAAI,QAAQ,MAAM;EAEpB,IAAI,MAAM,gBAAgB,KAAA,GACxB,IAAI,cAAc,MAAM;EAE1B,IAAI,MAAM,aAAa,KAAA,GACrB,IAAI,WAAW,MAAM;EAEvB,IAAI,MAAM,WAAW,KAAA,GACnB,IAAI,SAAS,MAAM;EAGrB,IAAI,MAAM,qBAAqB,KAAA,GAC7B,IAAI,mBAAmB,MAAM;EAE/B,IAAI,MAAM,aAAa,KAAA,GAErB,IAAI,WAAW,GAAG,GAAG,QAAQ,SAAS,MAAM,KAAK,UAAU,MAAM,QAAQ,EAAE;EAG7E,MAAM,CAAC,WAAW,MAAM,KAAK,GAC1B,OAAO,OAAO,CAAC,CACf,IAAI,GAAG,CAAC,CACR,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CACxD,UAAU,EAAE,IAAI,QAAQ,GAAG,CAAC;EAC/B,IAAI,CAAC,SACH,OAAO;EAET,OAAO,KAAK,SAAS,OAAO,EAAE;CAChC;CAEA,MAAM,OAAO,OAAe,IAA8B;EAUxD,QAAO,MANe,KAAK,GACxB,OAAO,YAAY,CAAC,CACpB,MACC,IAAI,GAAG,aAAa,OAAO,KAAK,GAAG,GAAG,aAAa,IAAI,EAAE,GAAG,GAAG,aAAa,MAAM,QAAQ,CAAC,CAC7F,CAAC,CACA,UAAU,EAAE,IAAI,aAAa,GAAG,CAAC,EAAA,CACrB,SAAS;CAC1B;CASA,cAAc,OAAe,UAAqC;EAChE,OAAO,KAAK,IAAI,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ,CAAC;CAC9D;CAEA,MAAM,wBAAwB,OAAe,UAAuC;EAClF,OAAO,KAAK,GACT,OAAO,CAAC,CACR,KAAK,UAAU,CAAC,CAChB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,UAAU,QAAQ,CAAC,CAAC,CAAC,CAC1E,QAAQ,WAAW,UAAU,WAAW,GAAG;CAChD;CAEA,MAAM,aAAa,OAAe,YAA8C;EAC9E,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,UAAU,CAAC,CAChB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,IAAI,UAAU,CAAC,CAAC,CAAC,CACtE,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;CAEA,MAAM,WAAW,OAAe,UAA0C;EACxE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CAC9D,MAAM,CAAC;EACV,OAAO,OAAO;CAChB;;CAGA,MAAc,YAAY,IAAQ,OAAe,UAAqC;EACpF,OAAO,GACJ,OAAO,CAAC,CACR,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,UAAU,QAAQ,CAAC,CAAC,CAAC,CACpE,QAAQ,QAAQ,GAAG;CACxB;;CAGA,MAAc,aACZ,IACA,OACA,UACA,UACe;EACf,MAAM,CAAC,OAAO,MAAM,GACjB,OAAO,EAAE,IAAI,QAAQ,GAAG,CAAC,CAAC,CAC1B,KAAK,OAAO,CAAC,CACb,MACC,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,QAAQ,GAAG,GAAG,QAAQ,UAAU,QAAQ,CAAC,CACxF,CAAC,CACA,MAAM,CAAC;EACV,IAAI,CAAC,KACH,MAAM,IAAI,cAAc,UAAU,QAAQ;CAE9C;;CAGA,MAAc,sBACZ,IACA,OACA,YACA,UACe;EACf,MAAM,GACH,OAAO,cAAc,CAAC,CACtB,MAAM,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,GAAG,eAAe,YAAY,UAAU,CAAC,CAAC;EACxF,IAAI,SAAS,QACX,MAAM,GACH,OAAO,cAAc,CAAC,CACtB,OAAO,SAAS,KAAK,SAAS,OAAO;GAAE;GAAO;GAAY;GAAS,KAAK;EAAE,EAAE,CAAC;CAEpF;CAEA,aAAa,OAAe,UAAkB,OAAkC;EAC9E,OAAO,KAAK,GAAG,OAAO,OAAO;GAK3B,MAAM,WAAU,MAJO,GACpB,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,CAAC,CAC5B,KAAK,OAAO,CAAC,CACb,MAAM,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,UAAU,QAAQ,CAAC,CAAC,EAAA,CAC7C,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,GAAG,GAAG,EAAE,IAAI;GACxE,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,OAAO;IAAE;IAAO;IAAU;IAAO,KAAK;GAAQ,CAAC;GACxE,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAEA,aACE,OACA,UACA,UACA,OACmB;EACnB,OAAO,KAAK,GAAG,OAAO,OAAO;GAC3B,MAAM,KAAK,aAAa,IAAI,OAAO,UAAU,QAAQ;GACrD,MAAM,GACH,OAAO,OAAO,CAAC,CACf,IAAI,EAAE,MAAM,CAAC,CAAC,CACd,MACC,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,QAAQ,GAAG,GAAG,QAAQ,UAAU,QAAQ,CAAC,CACxF;GACF,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAEA,aAAa,OAAe,UAAkB,UAAqC;EACjF,OAAO,KAAK,GAAG,OAAO,OAAO;GAC3B,MAAM,KAAK,aAAa,IAAI,OAAO,UAAU,QAAQ;GAQrD,MAAM,eAAc,MAJO,GACxB,OAAO,EAAE,IAAI,WAAW,GAAG,CAAC,CAAC,CAC7B,KAAK,UAAU,CAAC,CAChB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,UAAU,QAAQ,CAAC,CAAC,EAAA,CAC3C,KAAK,MAAM,EAAE,EAAE;GAEhD,IAAI,YAAY,QAAQ;IACtB,MAAM,GACH,OAAO,cAAc,CAAC,CACtB,MACC,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,QAAQ,eAAe,YAAY,WAAW,CAAC,CACtF;IACF,MAAM,GACH,OAAO,UAAU,CAAC,CAClB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,UAAU,QAAQ,CAAC,CAAC;GAC9E;GAEA,MAAM,GACH,OAAO,OAAO,CAAC,CACf,MACC,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI,QAAQ,GAAG,GAAG,QAAQ,UAAU,QAAQ,CAAC,CACxF;GACF,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAEA,eAAe,OAAe,UAAkB,YAAyC;EACvF,OAAO,KAAK,GAAG,OAAO,OAAO;GAG3B,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KACrC,MAAM,GACH,OAAO,OAAO,CAAC,CACf,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CACf,MACC,IACE,GAAG,QAAQ,OAAO,KAAK,GACvB,GAAG,QAAQ,UAAU,QAAQ,GAC7B,GAAG,QAAQ,IAAI,WAAW,EAAG,CAC/B,CACF;GAEJ,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAEA,kBACE,OACA,UACA,UACA,YACmB;EACnB,OAAO,KAAK,GAAG,OAAO,OAAO;GAC3B,MAAM,KAAK,aAAa,IAAI,OAAO,UAAU,QAAQ;GAGrD,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KACrC,MAAM,GACH,OAAO,UAAU,CAAC,CAClB,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CACtB,MACC,IACE,GAAG,WAAW,OAAO,KAAK,GAC1B,GAAG,WAAW,UAAU,QAAQ,GAChC,GAAG,WAAW,IAAI,WAAW,EAAG,CAClC,CACF;GAEJ,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KACrC,MAAM,GACH,OAAO,UAAU,CAAC,CAClB,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CACf,MACC,IACE,GAAG,WAAW,OAAO,KAAK,GAC1B,GAAG,WAAW,UAAU,QAAQ,GAChC,GAAG,WAAW,IAAI,WAAW,EAAG,CAClC,CACF;GAEJ,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAEA,aACE,OACA,UACA,UACA,OACA,YACoD;EACpD,OAAO,KAAK,GAAG,OAAO,OAAO;GAC3B,MAAM,KAAK,aAAa,IAAI,OAAO,UAAU,QAAQ;GAErD,IAAI;GACJ,IAAI,YAAY;IACd,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,EAAE,IAAI,WAAW,GAAG,CAAC,CAAC,CAC7B,KAAK,UAAU,CAAC,CAChB,MACC,IACE,GAAG,WAAW,OAAO,KAAK,GAC1B,GAAG,WAAW,IAAI,UAAU,GAC5B,GAAG,WAAW,UAAU,QAAQ,CAClC,CACF,CAAC,CACA,MAAM,CAAC;IACV,IAAI,CAAC,UACH,MAAM,IAAI,cAAc,YAAY,UAAU;IAGhD,MAAM,GACH,OAAO,UAAU,CAAC,CAClB,IAAI,EAAE,UAAU,MAAM,YAAY,KAAK,CAAC,CAAC,CACzC,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,IAAI,UAAU,CAAC,CAAC;IACxE,IAAI,MAAM,aAAa,KAAA,GACrB,MAAM,KAAK,sBAAsB,IAAI,OAAO,YAAY,MAAM,QAAQ;IAExE,UAAU;GACZ,OAAO;IAKL,MAAM,WAAU,MAJO,GACpB,OAAO,EAAE,KAAK,WAAW,IAAI,CAAC,CAAC,CAC/B,KAAK,UAAU,CAAC,CAChB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,UAAU,QAAQ,CAAC,CAAC,EAAA,CACnD,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,GAAG,GAAG,EAAE,IAAI;IAExE,MAAM,CAAC,OAAO,MAAM,GACjB,OAAO,UAAU,CAAC,CAClB,OAAO;KAAE;KAAO;KAAU;KAAU,KAAK;KAAS,UAAU,MAAM,YAAY;IAAK,CAAC,CAAC,CACrF,UAAU,EAAE,IAAI,WAAW,GAAG,CAAC;IAClC,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,2BAA2B;IAE7C,MAAM,KAAK,sBAAsB,IAAI,OAAO,IAAI,IAAI,MAAM,YAAY,CAAC,CAAC;IACxE,UAAU,IAAI;GAChB;GAEA,MAAM,CAAC,YAAY,MAAM,GACtB,OAAO,CAAC,CACR,KAAK,UAAU,CAAC,CAChB,MAAM,IAAI,GAAG,WAAW,OAAO,KAAK,GAAG,GAAG,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC,CACnE,MAAM,CAAC;GACV,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,+BAA+B;GAEjD,OAAO;IAAE,SAAS,MAAM,KAAK,YAAY,IAAI,OAAO,QAAQ;IAAG;GAAS;EAC1E,CAAC;CACH;CAEA,eACE,OACA,UACA,UACA,YACmB;EACnB,OAAO,KAAK,GAAG,OAAO,OAAO;GAC3B,MAAM,KAAK,aAAa,IAAI,OAAO,UAAU,QAAQ;GAGrD,MAAM,GACH,OAAO,cAAc,CAAC,CACtB,MAAM,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,GAAG,eAAe,YAAY,UAAU,CAAC,CAAC;GACxF,MAAM,GACH,OAAO,UAAU,CAAC,CAClB,MACC,IACE,GAAG,WAAW,OAAO,KAAK,GAC1B,GAAG,WAAW,IAAI,UAAU,GAC5B,GAAG,WAAW,UAAU,QAAQ,CAClC,CACF;GAEF,OAAO,KAAK,YAAY,IAAI,OAAO,QAAQ;EAC7C,CAAC;CACH;CAIA,MAAM,cAAc,OAAe,OAAoD;EACrF,MAAM,aAAoB,CAAC,GAAG,UAAU,OAAO,KAAK,CAAC;EACrD,IAAI,MAAM,QACR,WAAW,KAAK,GAAG,UAAU,QAAQ,MAAM,MAAM,CAAC;EAEpD,IAAI,MAAM,UACR,WAAW,KAAK,GAAG,UAAU,UAAU,MAAM,QAAQ,CAAC;EAGxD,MAAM,SAAS,MAAM,QAAQ,KAAK;EAClC,IAAI,QAAQ;GACV,MAAM,OAAO,IAAI,OAAO;GACxB,MAAM,QAAQ,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,MAAM,UAAU,UAAU,IAAI,CAAC;GAC9E,IAAI,OACF,WAAW,KAAK,KAAK;EAEzB;EAEA,MAAM,QAAQ,IAAI,GAAG,UAAU;EAG/B,IAAI,UAAU;EACd,IAAI,YAA4B;EAChC,IAAI,MAAM,MAAM;GACd,MAAM,SAAS,MAAM,KAAK,WAAW,GAAG;GACxC,MAAM,MAAM,SAAS,MAAM,KAAK,MAAM,CAAC,IAAI,MAAM;GACjD,IAAI,OAAO,qBAAqB;IAC9B,UAAU;IACV,YAAY,SAAS,SAAS;GAChC;EACF;EACA,MAAM,WAAW,oBAAoB,YAAY,UAAU;EAC3D,MAAM,UAAU,cAAc,SAAS,KAAK,QAAQ,IAAI,IAAI,QAAQ;EAEpE,MAAM,OAAO,MAAM,KAAK,GACrB,OAAO,CAAC,CACR,KAAK,SAAS,CAAC,CACf,MAAM,KAAK,CAAC,CACZ,QAAQ,OAAO,CAAC,CAChB,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ;EAE3C,MAAM,CAAC,YAAY,MAAM,KAAK,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,MAAM,KAAK;EAEvF,OAAO;GACL,MAAM,KAAK,IAAI,UAAU;GACzB,OAAO,UAAU,SAAS;GAC1B,MAAM,MAAM;GACZ,UAAU,MAAM;EAClB;CACF;CAEA,MAAM,YAAY,OAAe,IAAsC;EACrE,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,SAAS,CAAC,CACf,MAAM,IAAI,GAAG,UAAU,OAAO,KAAK,GAAG,GAAG,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAC5D,MAAM,CAAC;EACV,OAAO,MAAM,WAAW,GAAG,IAAI;CACjC;CAEA,MAAM,eACJ,OACA,OACA,MACmB;EAGnB,MAAM,KAAK,MAAM,UAAU;EAC3B,MAAM,KAAK,GAAG,OAAO,YAAY,CAAC,CAAC,OAAO;GAAE;GAAO;GAAI,MAAM;EAAW,CAAC;EACzE,MAAM,KAAK,GAAG,OAAO,SAAS,CAAC,CAAC,OAAO;GACrC;GACA;GACA,OAAO,MAAM;GACb;GACA,aAAa,MAAM,eAAe;GAClC,UAAU,MAAM,YAAY;EAC9B,CAAC;EACD,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO,EAAE;EAChD,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,iCAAiC;EAEnD,OAAO;CACT;CAEA,MAAM,eACJ,OACA,IACA,OAC0B;EAC1B,MAAM,MAA+B,EAAE,2BAAW,IAAI,KAAK,EAAE;EAC7D,IAAI,MAAM,UAAU,KAAA,GAClB,IAAI,QAAQ,MAAM;EAEpB,IAAI,MAAM,gBAAgB,KAAA,GACxB,IAAI,cAAc,MAAM;EAE1B,IAAI,MAAM,aAAa,KAAA,GACrB,IAAI,WAAW,MAAM;EAEvB,IAAI,MAAM,WAAW,KAAA,GACnB,IAAI,SAAS,MAAM;EAErB,IAAI,MAAM,qBAAqB,KAAA,GAC7B,IAAI,mBAAmB,MAAM;EAG/B,MAAM,CAAC,WAAW,MAAM,KAAK,GAC1B,OAAO,SAAS,CAAC,CACjB,IAAI,GAAG,CAAC,CACR,MAAM,IAAI,GAAG,UAAU,OAAO,KAAK,GAAG,GAAG,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAC5D,UAAU,EAAE,IAAI,UAAU,GAAG,CAAC;EACjC,IAAI,CAAC,SACH,OAAO;EAET,OAAO,KAAK,YAAY,OAAO,EAAE;CACnC;CAEA,MAAM,eAAe,OAAe,IAA8B;EAahE,QAAO,MAVe,KAAK,GACxB,OAAO,YAAY,CAAC,CACpB,MACC,IACE,GAAG,aAAa,OAAO,KAAK,GAC5B,GAAG,aAAa,IAAI,EAAE,GACtB,GAAG,aAAa,MAAM,UAAU,CAClC,CACF,CAAC,CACA,UAAU,EAAE,IAAI,aAAa,GAAG,CAAC,EAAA,CACrB,SAAS;CAC1B;CAEA,MAAM,mBAAmB,OAAe,YAA8C;EACpF,OAAO,KAAK,GACT,OAAO,CAAC,CACR,KAAK,cAAc,CAAC,CACpB,MAAM,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,GAAG,eAAe,YAAY,UAAU,CAAC,CAAC,CAAC,CACtF,QAAQ,IAAI,eAAe,GAAG,CAAC;CACpC;;CAGA,MAAc,eAAe,OAAe,YAAmC;EAC7E,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,EAAE,IAAI,UAAU,GAAG,CAAC,CAAC,CAC5B,KAAK,SAAS,CAAC,CACf,MAAM,IAAI,GAAG,UAAU,OAAO,KAAK,GAAG,GAAG,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC,CACpE,MAAM,CAAC;EACV,IAAI,CAAC,KACH,MAAM,IAAI,cAAc,YAAY,UAAU;CAElD;CAEA,MAAM,iBACJ,OACA,YACA,OAC0B;EAC1B,MAAM,KAAK,eAAe,OAAO,UAAU;EAC3C,MAAM,CAAC,UAAU,MAAM,KAAK,GACzB,OAAO,EAAE,QAAQ,GAAkB,OAAO,eAAe,IAAI,GAAG,CAAC,CAAC,CAClE,KAAK,cAAc,CAAC,CACpB,MAAM,IAAI,GAAG,eAAe,OAAO,KAAK,GAAG,GAAG,eAAe,YAAY,UAAU,CAAC,CAAC;EACxF,MAAM,WAAW,QAAQ,UAAU,MAAM;EACzC,IAAI;GACF,MAAM,KAAK,GAAG,OAAO,cAAc,CAAC,CAAC,OAAO;IAC1C;IACA;IACA,SAAS,MAAM;IACf,KAAK;IACL,aAAa,MAAM,eAAe;GACpC,CAAC;EACH,SAAS,KAAK;GACZ,IAAI,kBAAkB,GAAG,GACvB,MAAM,IAAI,cAAc,8CAA8C;GAExE,MAAM;EACR;EACA,OAAO,KAAK,mBAAmB,OAAO,UAAU;CAClD;CAEA,MAAM,oBACJ,OACA,YACA,SAC0B;EAC1B,MAAM,KAAK,eAAe,OAAO,UAAU;EAC3C,MAAM,KAAK,GACR,OAAO,cAAc,CAAC,CACtB,MACC,IACE,GAAG,eAAe,OAAO,KAAK,GAC9B,GAAG,eAAe,YAAY,UAAU,GACxC,GAAG,eAAe,SAAS,OAAO,CACpC,CACF;EACF,OAAO,KAAK,mBAAmB,OAAO,UAAU;CAClD;CAEA,MAAM,oBACJ,OACA,YACA,SACA,aAC0B;EAC1B,MAAM,KAAK,eAAe,OAAO,UAAU;EAC3C,MAAM,KAAK,GACR,OAAO,cAAc,CAAC,CACtB,IAAI,EAAE,YAAY,CAAC,CAAC,CACpB,MACC,IACE,GAAG,eAAe,OAAO,KAAK,GAC9B,GAAG,eAAe,YAAY,UAAU,GACxC,GAAG,eAAe,SAAS,OAAO,CACpC,CACF;EACF,OAAO,KAAK,mBAAmB,OAAO,UAAU;CAClD;CAEA,MAAM,sBACJ,OACA,YACA,UAC0B;EAC1B,MAAM,KAAK,eAAe,OAAO,UAAU;EAE3C,KAAK,MAAM,CAAC,KAAK,YAAY,SAAS,QAAQ,GAC5C,MAAM,KAAK,GACR,OAAO,cAAc,CAAC,CACtB,IAAI,EAAE,IAAI,CAAC,CAAC,CACZ,MACC,IACE,GAAG,eAAe,OAAO,KAAK,GAC9B,GAAG,eAAe,YAAY,UAAU,GACxC,GAAG,eAAe,SAAS,OAAO,CACpC,CACF;EAEJ,OAAO,KAAK,mBAAmB,OAAO,UAAU;CAClD;AACF"}
|
|
1
|
+
{"version":3,"file":"content.js","names":[],"sources":["../../src/repositories/content.ts"],"sourcesContent":["// content — Drizzle repository for the course aggregate root (implements the core\n// outbound `ContentRepository` port). Org-scoped: every method takes the domain\n// `organizations.id` and constrains its queries to that tenant. Rows come back\n// as stored — derived counts and cross-entity composition live in reporting.\nimport {\n eq,\n and,\n sql,\n count,\n asc,\n desc,\n ilike,\n or,\n inArray,\n type SQL,\n type AnyColumn,\n} from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { DbExecutor, Tx } from \"../client.js\";\nimport type {\n Activity,\n ActivityAsset,\n AddDownloadAssetInput,\n Bundle,\n BundleItem,\n ContentRepository,\n Course,\n CourseSettings,\n CourseStatus,\n CreateBundleInput,\n CreateCourseInput,\n CreateDownloadInput,\n Download,\n DownloadAsset,\n DownloadStatus,\n ListBundlesQuery,\n ListCoursesQuery,\n ListDownloadsQuery,\n Module,\n Page,\n SaveActivityInput,\n UpdateBundleInput,\n UpdateCourseInput,\n UpdateDownloadInput,\n} from \"@headless-lms/core/content\";\nimport { courseSettingsDefaults, type Setting } from \"@headless-lms/core/schemas\";\nimport {\n bundles,\n bundleItems,\n contentItems,\n courses,\n modules,\n activities,\n activityAssets,\n downloads,\n downloadAssets,\n} from \"../schema/content.js\";\nimport { settings } from \"../schema/settings.js\";\nimport { deepMerge } from \"./settings.js\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { NotFoundError, ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, translateDbErrors } from \"./pg-errors.js\";\n\n// A course's settings live in the shared `settings` table (namespace\n// 'content', scoped by course id) — joined here so the repository answers in\n// whole courses regardless of where the pieces are stored.\nconst SETTINGS_NAMESPACE = \"content\";\n\nconst courseSettingsJoin = and(\n eq(settings.orgId, courses.orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, courses.id),\n);\n\nfunction toCourse(\n row: typeof courses.$inferSelect,\n storedSettings: Record<string, unknown> | null,\n): Course {\n return {\n ...row,\n status: row.status as CourseStatus,\n settings: { ...courseSettingsDefaults, ...storedSettings } as Course[\"settings\"],\n };\n}\n\n// Columns a caller may sort by. Default falls back to createdAt desc.\nconst sortColumns: Record<string, AnyColumn | SQL> = {\n title: courses.title,\n slug: courses.slug,\n status: courses.status,\n category: courses.category,\n createdAt: courses.createdAt,\n updatedAt: courses.updatedAt,\n};\n\nconst downloadSortColumns: Record<string, AnyColumn | SQL> = {\n title: downloads.title,\n slug: downloads.slug,\n status: downloads.status,\n category: downloads.category,\n createdAt: downloads.createdAt,\n updatedAt: downloads.updatedAt,\n};\n\nfunction toDownload(row: typeof downloads.$inferSelect): Download {\n return { ...row, status: row.status as DownloadStatus };\n}\n\nconst bundleSortColumns: Record<string, AnyColumn | SQL> = {\n name: bundles.name,\n createdAt: bundles.createdAt,\n updatedAt: bundles.updatedAt,\n};\n\nexport class DrizzleContentRepository implements ContentRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it\n * nests as a savepoint inside the surrounding transaction. */\n private tx<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {\n return (this.db as NodePgDatabase).transaction(fn);\n }\n\n async listCourses(orgId: string, query: ListCoursesQuery): Promise<Page<Course>> {\n const conditions: SQL[] = [eq(courses.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(courses.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(courses.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(courses.title, like), ilike(courses.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in sortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = sortColumns[sortKey] ?? courses.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select({ course: courses, settingsValue: settings.value })\n .from(courses)\n .leftJoin(settings, courseSettingsJoin)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);\n\n return {\n rows: rows.map((row) => toCourse(row.course, row.settingsValue)),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findCourseById(orgId: string, id: string): Promise<Course | null> {\n const [row] = await this.db\n .select({ course: courses, settingsValue: settings.value })\n .from(courses)\n .leftJoin(settings, courseSettingsJoin)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .limit(1);\n return row ? toCourse(row.course, row.settingsValue) : null;\n }\n\n async createCourse(orgId: string, input: CreateCourseInput, slug: string): Promise<Course> {\n // Registry row + concrete row share one id. Both inserts run on the same\n // executor — mutations reach this repository tx-bound (ContentUnitOfWork),\n // so they commit or roll back together.\n const id = genId(\"course\");\n await this.db.insert(contentItems).values({ orgId, id, type: \"course\" });\n const [inserted] = await this.db\n .insert(courses)\n .values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? \"\",\n category: input.category ?? \"\",\n })\n .returning({ id: courses.id });\n if (!inserted) {\n throw new Error(\"failed to insert course\");\n }\n const created = await this.findCourseById(orgId, inserted.id);\n if (!created) {\n throw new Error(\"failed to load created course\");\n }\n return created;\n }\n\n async updateCourse(orgId: string, id: string, patch: UpdateCourseInput): Promise<Course | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n // null is meaningful here — it clears the cover — so only `undefined` skips.\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(courses)\n .set(set)\n .where(and(eq(courses.orgId, orgId), eq(courses.id, id)))\n .returning({ id: courses.id });\n if (!updated) {\n return null;\n }\n return this.findCourseById(orgId, id);\n }\n\n async patchCourseSettings(\n orgId: string,\n id: string,\n value: Partial<CourseSettings>,\n ): Promise<CourseSettings> {\n const key = and(\n eq(settings.orgId, orgId),\n eq(settings.namespace, SETTINGS_NAMESPACE),\n eq(settings.scopeId, id),\n );\n\n // Merge in JS under a row lock, same as the settings repository: the read\n // and the write must be one atomic unit or concurrent patches would\n // clobber each other's sibling keys.\n const merged = await this.tx(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({\n orgId,\n namespace: SETTINGS_NAMESPACE,\n scopeId: id,\n value: value as Setting[\"value\"],\n })\n .onConflictDoNothing()\n .returning();\n if (inserted) {\n return inserted.value;\n }\n [existing] = await tx.select().from(settings).where(key).for(\"update\");\n if (!existing) {\n throw new Error(\"course settings patch: row vanished mid-transaction\");\n }\n }\n\n const [updated] = await tx\n .update(settings)\n .set({ value: deepMerge(existing.value, value) as Setting[\"value\"], updatedAt: new Date() })\n .where(key)\n .returning();\n if (!updated) {\n throw new Error(\"course settings patch returned no row\");\n }\n return updated.value;\n });\n\n return { ...courseSettingsDefaults, ...merged } as CourseSettings;\n }\n\n async deleteCourse(orgId: string, id: string): Promise<boolean> {\n // Deletes go through the registry: cascades to the course row and its\n // entitlements in one statement. Never delete from `courses` directly —\n // that would strand the registry row.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, \"course\"),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n // --- modules & activities ----------------------------------------------\n // Every mutation returns the course's full ordered module list (by `seq`),\n // so `listModules` runs at the end of every mutation within the same\n // transaction. An Activity's media is the many-to-many `activity_assets`\n // join: `saveActivity` upserts the row and replaces its asset links; deletes\n // drop the links first (they FK the activity), then the activity rows.\n\n listCourseModules(orgId: string, courseId: string): Promise<Module[]> {\n return this.tx((tx) => this.listModules(tx, orgId, courseId));\n }\n\n async listActivitiesForCourse(orgId: string, courseId: string): Promise<Activity[]> {\n return this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activities.moduleId, activities.seq);\n }\n\n // Bare join-table rows; the activities join only scopes them to the course.\n async listActivityAssetsForCourse(orgId: string, courseId: string): Promise<ActivityAsset[]> {\n return this.db\n .select({\n orgId: activityAssets.orgId,\n id: activityAssets.id,\n activityId: activityAssets.activityId,\n assetId: activityAssets.assetId,\n seq: activityAssets.seq,\n createdAt: activityAssets.createdAt,\n updatedAt: activityAssets.updatedAt,\n })\n .from(activityAssets)\n .innerJoin(\n activities,\n and(\n eq(activities.orgId, activityAssets.orgId),\n eq(activities.id, activityAssets.activityId),\n ),\n )\n .where(and(eq(activityAssets.orgId, orgId), eq(activities.courseId, courseId)))\n .orderBy(activityAssets.activityId, activityAssets.seq);\n }\n\n async findActivity(orgId: string, activityId: string): Promise<Activity | null> {\n const [row] = await this.db\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)))\n .limit(1);\n return row ?? null;\n }\n\n async findModule(orgId: string, moduleId: string): Promise<Module | null> {\n const [row] = await this.db\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId)))\n .limit(1);\n return row ?? null;\n }\n\n /** The course's full ordered module list. */\n private async listModules(tx: Tx, orgId: string, courseId: string): Promise<Module[]> {\n return tx\n .select()\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)))\n .orderBy(modules.seq);\n }\n\n /** Assert the module belongs to the org + course; throw otherwise. */\n private async assertModule(\n tx: Tx,\n orgId: string,\n courseId: string,\n moduleId: string,\n ): Promise<void> {\n const [row] = await tx\n .select({ id: modules.id })\n .from(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n )\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Module\", moduleId);\n }\n }\n\n /** Replace an activity's ordered asset links with `assetIds`. */\n private async replaceActivityAssets(\n tx: Tx,\n orgId: string,\n activityId: string,\n assetIds: string[],\n ): Promise<void> {\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n if (assetIds.length) {\n await tx\n .insert(activityAssets)\n .values(assetIds.map((assetId, i) => ({ orgId, activityId, assetId, seq: i })));\n }\n }\n\n createModule(orgId: string, courseId: string, title: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n const existing = await tx\n .select({ seq: modules.seq })\n .from(modules)\n .where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n await tx.insert(modules).values({ orgId, courseId, title, seq: nextSeq });\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n updateModule(\n orgId: string,\n courseId: string,\n moduleId: string,\n title: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n await tx\n .update(modules)\n .set({ title })\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n deleteModule(orgId: string, courseId: string, moduleId: string): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the module's activities: their asset links first (they FK the\n // activity), then the activity rows, then the module.\n const activityRows = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const activityIds = activityRows.map((a) => a.id);\n\n if (activityIds.length) {\n await tx\n .delete(activityAssets)\n .where(\n and(eq(activityAssets.orgId, orgId), inArray(activityAssets.activityId, activityIds)),\n );\n await tx\n .delete(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n }\n\n await tx\n .delete(modules)\n .where(\n and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)),\n );\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderModules(orgId: string, courseId: string, orderedIds: string[]): Promise<Module[]> {\n return this.tx(async (tx) => {\n // Two-phase to dodge the unique(org_id, id) is fine, but modules have no\n // unique(seq); still, park then assign to stay consistent with activities.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(modules)\n .set({ seq: i })\n .where(\n and(\n eq(modules.orgId, orgId),\n eq(modules.courseId, courseId),\n eq(modules.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n reorderActivities(\n orgId: string,\n courseId: string,\n moduleId: string,\n orderedIds: string[],\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n // Two-phase to dodge the unique(org_id, module_id, seq) constraint mid-swap:\n // park rows at negative seqs, then assign the final 0..n-1.\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: -(i + 1) })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n for (let i = 0; i < orderedIds.length; i++) {\n await tx\n .update(activities)\n .set({ seq: i })\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.moduleId, moduleId),\n eq(activities.id, orderedIds[i]!),\n ),\n );\n }\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n saveActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n input: SaveActivityInput,\n activityId?: string,\n ): Promise<{ modules: Module[]; activity: Activity }> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n let savedId: string;\n if (activityId) {\n const [existing] = await tx\n .select({ id: activities.id })\n .from(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n )\n .limit(1);\n if (!existing) {\n throw new NotFoundError(\"Activity\", activityId);\n }\n\n await tx\n .update(activities)\n .set({ settings: input.settings ?? null })\n .where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)));\n if (input.assetIds !== undefined) {\n await this.replaceActivityAssets(tx, orgId, activityId, input.assetIds);\n }\n savedId = activityId;\n } else {\n const existing = await tx\n .select({ seq: activities.seq })\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));\n const nextSeq = existing.reduce((max, r) => Math.max(max, r.seq), -1) + 1;\n\n const [ins] = await tx\n .insert(activities)\n .values({ orgId, moduleId, courseId, seq: nextSeq, settings: input.settings ?? null })\n .returning({ id: activities.id });\n if (!ins) {\n throw new Error(\"failed to insert activity\");\n }\n await this.replaceActivityAssets(tx, orgId, ins.id, input.assetIds ?? []);\n savedId = ins.id;\n }\n\n const [activity] = await tx\n .select()\n .from(activities)\n .where(and(eq(activities.orgId, orgId), eq(activities.id, savedId)))\n .limit(1);\n if (!activity) {\n throw new Error(\"failed to load saved activity\");\n }\n return { modules: await this.listModules(tx, orgId, courseId), activity };\n });\n }\n\n deleteActivity(\n orgId: string,\n courseId: string,\n moduleId: string,\n activityId: string,\n ): Promise<Module[]> {\n return this.tx(async (tx) => {\n await this.assertModule(tx, orgId, courseId, moduleId);\n\n // Drop the asset links first (they FK the activity), then the activity.\n await tx\n .delete(activityAssets)\n .where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));\n await tx\n .delete(activities)\n .where(\n and(\n eq(activities.orgId, orgId),\n eq(activities.id, activityId),\n eq(activities.moduleId, moduleId),\n ),\n );\n\n return this.listModules(tx, orgId, courseId);\n });\n }\n\n // --- downloads --------------------------------------------------------\n\n async listDownloads(orgId: string, query: ListDownloadsQuery): Promise<Page<Download>> {\n const conditions: SQL[] = [eq(downloads.orgId, orgId)];\n if (query.status) {\n conditions.push(eq(downloads.status, query.status));\n }\n if (query.category) {\n conditions.push(eq(downloads.category, query.category));\n }\n\n const search = query.search?.trim();\n if (search) {\n const like = `%${search}%`;\n const match = or(ilike(downloads.title, like), ilike(downloads.category, like));\n if (match) {\n conditions.push(match);\n }\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in downloadSortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = downloadSortColumns[sortKey] ?? downloads.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(downloads)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(downloads).where(where);\n\n return {\n rows: rows.map(toDownload),\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async getDownload(orgId: string, id: string): Promise<Download | null> {\n const [row] = await this.db\n .select()\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .limit(1);\n return row ? toDownload(row) : null;\n }\n\n async createDownload(orgId: string, input: CreateDownloadInput, slug: string): Promise<Download> {\n // Registry row + concrete row share one id, same as courses. Both inserts\n // run on the same executor — mutations reach this repository tx-bound.\n const id = genId(\"download\");\n await this.db.insert(contentItems).values({ orgId, id, type: \"download\" });\n await this.db.insert(downloads).values({\n orgId,\n id,\n title: input.title,\n slug,\n description: input.description ?? \"\",\n category: input.category ?? \"\",\n });\n const created = await this.getDownload(orgId, id);\n if (!created) {\n throw new Error(\"failed to load created download\");\n }\n return created;\n }\n\n async updateDownload(\n orgId: string,\n id: string,\n patch: UpdateDownloadInput,\n ): Promise<Download | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.title !== undefined) {\n set.title = patch.title;\n }\n if (patch.description !== undefined) {\n set.description = patch.description;\n }\n if (patch.category !== undefined) {\n set.category = patch.category;\n }\n if (patch.status !== undefined) {\n set.status = patch.status;\n }\n if (patch.thumbnailAssetId !== undefined) {\n set.thumbnailAssetId = patch.thumbnailAssetId;\n }\n\n const [updated] = await this.db\n .update(downloads)\n .set(set)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, id)))\n .returning({ id: downloads.id });\n if (!updated) {\n return null;\n }\n return this.getDownload(orgId, id);\n }\n\n async deleteDownload(orgId: string, id: string): Promise<boolean> {\n // Through the registry, so the cascade reaches the download row, its asset\n // links, and the grants. Never delete from `downloads` directly.\n const deleted = await this.db\n .delete(contentItems)\n .where(\n and(\n eq(contentItems.orgId, orgId),\n eq(contentItems.id, id),\n eq(contentItems.type, \"download\"),\n ),\n )\n .returning({ id: contentItems.id });\n return deleted.length > 0;\n }\n\n async listDownloadAssets(orgId: string, downloadId: string): Promise<DownloadAsset[]> {\n return this.db\n .select()\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)))\n .orderBy(asc(downloadAssets.seq));\n }\n\n /** Assert the download exists in the org; throw otherwise. */\n private async assertDownload(orgId: string, downloadId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: downloads.id })\n .from(downloads)\n .where(and(eq(downloads.orgId, orgId), eq(downloads.id, downloadId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Download\", downloadId);\n }\n }\n\n async addDownloadAsset(\n orgId: string,\n downloadId: string,\n input: AddDownloadAssetInput,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n const [maxRow] = await this.db\n .select({ maxSeq: sql<number | null>`max(${downloadAssets.seq})` })\n .from(downloadAssets)\n .where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)));\n const nextSeq = (maxRow?.maxSeq ?? -1) + 1;\n try {\n await this.db.insert(downloadAssets).values({\n orgId,\n downloadId,\n assetId: input.assetId,\n seq: nextSeq,\n displayName: input.displayName ?? null,\n });\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"This asset is already linked to the download\");\n }\n throw err;\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async removeDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .delete(downloadAssets)\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async renameDownloadAsset(\n orgId: string,\n downloadId: string,\n assetId: string,\n displayName: string | null,\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n await this.db\n .update(downloadAssets)\n .set({ displayName })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n async reorderDownloadAssets(\n orgId: string,\n downloadId: string,\n assetIds: string[],\n ): Promise<DownloadAsset[]> {\n await this.assertDownload(orgId, downloadId);\n // The service has already verified this is exactly the current set.\n for (const [seq, assetId] of assetIds.entries()) {\n await this.db\n .update(downloadAssets)\n .set({ seq })\n .where(\n and(\n eq(downloadAssets.orgId, orgId),\n eq(downloadAssets.downloadId, downloadId),\n eq(downloadAssets.assetId, assetId),\n ),\n );\n }\n return this.listDownloadAssets(orgId, downloadId);\n }\n\n // --- bundles ----------------------------------------------------------\n\n async listBundles(orgId: string, query: ListBundlesQuery): Promise<Page<Bundle>> {\n const conditions: SQL[] = [eq(bundles.orgId, orgId)];\n\n const search = query.search?.trim();\n if (search) {\n conditions.push(ilike(bundles.name, `%${search}%`));\n }\n\n const where = and(...conditions);\n\n // Resolve sort: `-` prefix = desc, default createdAt desc.\n let sortKey = \"createdAt\";\n let direction: \"asc\" | \"desc\" = \"desc\";\n if (query.sort) {\n const isDesc = query.sort.startsWith(\"-\");\n const key = isDesc ? query.sort.slice(1) : query.sort;\n if (key in bundleSortColumns) {\n sortKey = key;\n direction = isDesc ? \"desc\" : \"asc\";\n }\n }\n const sortExpr = bundleSortColumns[sortKey] ?? bundles.createdAt;\n const orderBy = direction === \"desc\" ? desc(sortExpr) : asc(sortExpr);\n\n const rows = await this.db\n .select()\n .from(bundles)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize);\n\n const [totalRow] = await this.db.select({ value: count() }).from(bundles).where(where);\n\n return {\n rows,\n total: totalRow?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findBundleById(orgId: string, id: string): Promise<Bundle | null> {\n const [row] = await this.db\n .select()\n .from(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .limit(1);\n return row ?? null;\n }\n\n /** The bundle and its initial items are one write: an unknown content id\n * rolls the bundle back rather than leaving it created and half-filled. */\n async createBundle(orgId: string, input: CreateBundleInput): Promise<Bundle> {\n const contentIds = [...new Set(input.contentIds ?? [])];\n return this.tx(async (tx) => {\n // A bundle is not content: it groups registry rows, so it gets no\n // content_items row of its own.\n const [row] = await tx\n .insert(bundles)\n .values({ orgId, id: genId(\"bundle\"), name: input.name })\n .returning();\n if (!row) {\n throw new Error(\"failed to load created bundle\");\n }\n\n if (contentIds.length > 0) {\n // Against the registry, so any content type can be bundled.\n const found = await tx\n .select({ id: contentItems.id })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), inArray(contentItems.id, contentIds)));\n const known = new Set(found.map((item) => item.id));\n const missing = contentIds.find((id) => !known.has(id));\n if (missing) {\n throw new NotFoundError(\"Content\", missing);\n }\n\n await tx\n .insert(bundleItems)\n .values(contentIds.map((contentId) => ({ orgId, bundleId: row.id, contentId })));\n }\n\n return row;\n });\n }\n\n async updateBundle(orgId: string, id: string, patch: UpdateBundleInput): Promise<Bundle | null> {\n const set: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) {\n set.name = patch.name;\n }\n\n const [row] = await this.db\n .update(bundles)\n .set(set)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .returning();\n return row ?? null;\n }\n\n async deleteBundle(orgId: string, id: string): Promise<boolean> {\n // Cascades to the bundle's items and to the grants made on it. The content\n // itself is untouched — a bundle only groups registry rows.\n const deleted = await this.db\n .delete(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, id)))\n .returning({ id: bundles.id });\n return deleted.length > 0;\n }\n\n async listBundleItems(orgId: string, bundleId: string): Promise<BundleItem[]> {\n return this.db\n .select()\n .from(bundleItems)\n .where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId)))\n .orderBy(asc(bundleItems.createdAt));\n }\n\n /** Assert the bundle exists in the org; throw otherwise. */\n private async assertBundle(orgId: string, bundleId: string): Promise<void> {\n const [row] = await this.db\n .select({ id: bundles.id })\n .from(bundles)\n .where(and(eq(bundles.orgId, orgId), eq(bundles.id, bundleId)))\n .limit(1);\n if (!row) {\n throw new NotFoundError(\"Bundle\", bundleId);\n }\n }\n\n async addBundleItem(orgId: string, bundleId: string, contentId: string): Promise<BundleItem[]> {\n await this.assertBundle(orgId, bundleId);\n // Against the registry, so any content type can be bundled.\n const [item] = await this.db\n .select({ id: contentItems.id })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId)))\n .limit(1);\n if (!item) {\n throw new NotFoundError(\"Content\", contentId);\n }\n await this.db\n .insert(bundleItems)\n .values({ orgId, bundleId, contentId })\n .onConflictDoNothing({\n target: [bundleItems.orgId, bundleItems.bundleId, bundleItems.contentId],\n });\n return this.listBundleItems(orgId, bundleId);\n }\n\n async removeBundleItem(\n orgId: string,\n bundleId: string,\n contentId: string,\n ): Promise<BundleItem[]> {\n await this.assertBundle(orgId, bundleId);\n await this.db\n .delete(bundleItems)\n .where(\n and(\n eq(bundleItems.orgId, orgId),\n eq(bundleItems.bundleId, bundleId),\n eq(bundleItems.contentId, contentId),\n ),\n );\n return this.listBundleItems(orgId, bundleId);\n }\n}\ntranslateDbErrors(DrizzleContentRepository);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Logger } from "@headless-lms/core/shared/ports";
|
|
3
|
+
import { CourseAnalytics, CourseEnrollmentPoint, CoursesReportRepository } from "@headless-lms/core/reporting/courses";
|
|
4
|
+
//#region src/repositories/course-analytics.d.ts
|
|
5
|
+
declare class DrizzleCourseAnalyticsRepository implements CoursesReportRepository {
|
|
6
|
+
private readonly db;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
|
+
private courseExists;
|
|
10
|
+
analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
11
|
+
enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DrizzleCourseAnalyticsRepository };
|
|
15
|
+
//# sourceMappingURL=course-analytics.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { courses } from "../schema/content.js";
|
|
2
|
+
import { entitlements } from "../schema/entitlements.js";
|
|
3
|
+
import "../schema/index.js";
|
|
4
|
+
import { translateDbErrors } from "./pg-errors.js";
|
|
5
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
6
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
7
|
+
//#region src/repositories/course-analytics.ts
|
|
8
|
+
var DrizzleCourseAnalyticsRepository = class {
|
|
9
|
+
db;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(db, logger = noopLogger) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
async courseExists(orgId, courseId) {
|
|
16
|
+
const [row] = await this.db.select({ id: courses.id }).from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, courseId)));
|
|
17
|
+
return row !== void 0;
|
|
18
|
+
}
|
|
19
|
+
async analytics(orgId, courseId) {
|
|
20
|
+
if (!await this.courseExists(orgId, courseId)) return null;
|
|
21
|
+
const cohort = sql`
|
|
22
|
+
select org_user_id from entitlements
|
|
23
|
+
where org_id = ${orgId}
|
|
24
|
+
and content_id = ${courseId}
|
|
25
|
+
and status = 'active'
|
|
26
|
+
and (expires_at is null or expires_at >= now())
|
|
27
|
+
`;
|
|
28
|
+
const publishedActivities = sql`
|
|
29
|
+
select a.id, a.settings, a.seq, a.module_id
|
|
30
|
+
from activities a
|
|
31
|
+
where a.org_id = ${orgId}
|
|
32
|
+
and a.course_id = ${courseId}
|
|
33
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
34
|
+
`;
|
|
35
|
+
const summary = (await this.db.execute(sql`
|
|
36
|
+
with cohort as (${cohort}),
|
|
37
|
+
acts as (${publishedActivities}),
|
|
38
|
+
per_learner as (
|
|
39
|
+
select c.org_user_id,
|
|
40
|
+
count(pr.id) filter (where pr.completed_at is not null) as done
|
|
41
|
+
from cohort c
|
|
42
|
+
left join progress_records pr
|
|
43
|
+
on pr.org_id = ${orgId}
|
|
44
|
+
and pr.org_user_id = c.org_user_id
|
|
45
|
+
and pr.target_type = 'activity'
|
|
46
|
+
and pr.target_id in (select id from acts)
|
|
47
|
+
group by c.org_user_id
|
|
48
|
+
)
|
|
49
|
+
select
|
|
50
|
+
(select count(*) from cohort)::int as enrolled,
|
|
51
|
+
(
|
|
52
|
+
select count(distinct pr.org_user_id) from progress_records pr
|
|
53
|
+
where pr.org_id = ${orgId}
|
|
54
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
55
|
+
and (
|
|
56
|
+
(pr.target_type = 'activity' and pr.target_id in (select id from acts))
|
|
57
|
+
or (pr.target_type = 'course' and pr.target_id = ${courseId})
|
|
58
|
+
)
|
|
59
|
+
)::int as started,
|
|
60
|
+
(
|
|
61
|
+
select count(*) from progress_records pr
|
|
62
|
+
where pr.org_id = ${orgId}
|
|
63
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
64
|
+
and pr.target_type = 'course'
|
|
65
|
+
and pr.target_id = ${courseId}
|
|
66
|
+
and pr.completed_at is not null
|
|
67
|
+
)::int as completed,
|
|
68
|
+
coalesce((
|
|
69
|
+
select round(avg(least(done::numeric / nullif((select count(*) from acts), 0), 1) * 100))
|
|
70
|
+
from per_learner
|
|
71
|
+
), 0)::int as avg_progress
|
|
72
|
+
`)).rows[0];
|
|
73
|
+
const activities = (await this.db.execute(sql`
|
|
74
|
+
with cohort as (${cohort})
|
|
75
|
+
select
|
|
76
|
+
a.id,
|
|
77
|
+
coalesce(a.settings ->> 'title', '') as title,
|
|
78
|
+
m.id as module_id,
|
|
79
|
+
m.title as module_title,
|
|
80
|
+
count(pr.id)::int as started,
|
|
81
|
+
count(pr.id) filter (where pr.completed_at is not null)::int as completed
|
|
82
|
+
from activities a
|
|
83
|
+
join modules m on m.org_id = a.org_id and m.id = a.module_id
|
|
84
|
+
left join progress_records pr
|
|
85
|
+
on pr.org_id = ${orgId}
|
|
86
|
+
and pr.target_type = 'activity'
|
|
87
|
+
and pr.target_id = a.id
|
|
88
|
+
and pr.org_user_id in (select org_user_id from cohort)
|
|
89
|
+
where a.org_id = ${orgId}
|
|
90
|
+
and a.course_id = ${courseId}
|
|
91
|
+
and coalesce((a.settings ->> 'published')::boolean, true)
|
|
92
|
+
group by a.id, a.settings, m.id, m.title, m.seq, a.seq
|
|
93
|
+
order by m.seq, a.seq
|
|
94
|
+
`)).rows.map((r) => ({
|
|
95
|
+
activityId: r.id,
|
|
96
|
+
title: r.title,
|
|
97
|
+
moduleId: r.module_id,
|
|
98
|
+
moduleTitle: r.module_title,
|
|
99
|
+
started: Number(r.started),
|
|
100
|
+
completed: Number(r.completed)
|
|
101
|
+
}));
|
|
102
|
+
return {
|
|
103
|
+
enrolled: Number(summary?.enrolled ?? 0),
|
|
104
|
+
started: Number(summary?.started ?? 0),
|
|
105
|
+
completed: Number(summary?.completed ?? 0),
|
|
106
|
+
avgProgress: Number(summary?.avg_progress ?? 0),
|
|
107
|
+
activities
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async enrollments(orgId, courseId, days) {
|
|
111
|
+
if (!await this.courseExists(orgId, courseId)) return null;
|
|
112
|
+
return (await this.db.execute(sql`
|
|
113
|
+
select
|
|
114
|
+
to_char(d.day, 'YYYY-MM-DD') as date,
|
|
115
|
+
count(${entitlements.id})::int as count
|
|
116
|
+
from generate_series(
|
|
117
|
+
(now() at time zone 'utc')::date - (${days} - 1),
|
|
118
|
+
(now() at time zone 'utc')::date,
|
|
119
|
+
interval '1 day'
|
|
120
|
+
) as d(day)
|
|
121
|
+
left join ${entitlements}
|
|
122
|
+
on ${entitlements.orgId} = ${orgId}
|
|
123
|
+
and ${entitlements.contentId} = ${courseId}
|
|
124
|
+
and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
|
|
125
|
+
group by d.day
|
|
126
|
+
order by d.day
|
|
127
|
+
`)).rows.map((r) => ({
|
|
128
|
+
date: r.date,
|
|
129
|
+
count: Number(r.count)
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
translateDbErrors(DrizzleCourseAnalyticsRepository);
|
|
134
|
+
//#endregion
|
|
135
|
+
export { DrizzleCourseAnalyticsRepository };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=course-analytics.js.map
|