@headless-lms/adapter-db 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +8 -6
- package/dist/migrate.js.map +1 -1
- package/dist/repositories/admin.d.ts +23 -0
- package/dist/repositories/admin.js +393 -0
- package/dist/repositories/admin.js.map +1 -0
- package/dist/repositories/assets.d.ts +6 -2
- package/dist/repositories/assets.js +53 -9
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +12 -6
- package/dist/repositories/automations.js +79 -11
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +55 -0
- package/dist/repositories/chats.js +454 -0
- package/dist/repositories/chats.js.map +1 -0
- package/dist/repositories/content.d.ts +44 -7
- package/dist/repositories/content.js +458 -24
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/credentials.js +2 -0
- package/dist/repositories/credentials.js.map +1 -1
- package/dist/repositories/discussion.d.ts +11 -4
- package/dist/repositories/discussion.js +72 -15
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +36 -10
- package/dist/repositories/entitlements.js +238 -23
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/grant-covers.d.ts +20 -0
- package/dist/repositories/grant-covers.js +55 -0
- package/dist/repositories/grant-covers.js.map +1 -0
- package/dist/repositories/identity.d.ts +4 -2
- package/dist/repositories/identity.js +34 -1
- package/dist/repositories/identity.js.map +1 -1
- package/dist/repositories/integrations.d.ts +1 -1
- package/dist/repositories/integrations.js +2 -0
- package/dist/repositories/integrations.js.map +1 -1
- package/dist/repositories/member.d.ts +16 -0
- package/dist/repositories/member.js +63 -0
- package/dist/repositories/member.js.map +1 -0
- package/dist/repositories/members.js +4 -2
- package/dist/repositories/members.js.map +1 -1
- package/dist/repositories/notifications.d.ts +23 -0
- package/dist/repositories/notifications.js +70 -0
- package/dist/repositories/notifications.js.map +1 -0
- package/dist/repositories/org-user-profile.d.ts +214 -1
- package/dist/repositories/org-user-profile.js +23 -2
- package/dist/repositories/org-user-profile.js.map +1 -1
- package/dist/repositories/organizations.d.ts +6 -7
- package/dist/repositories/organizations.js +45 -52
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/outbox.js +3 -0
- package/dist/repositories/outbox.js.map +1 -1
- package/dist/repositories/pg-errors.d.ts +53 -5
- package/dist/repositories/pg-errors.js +99 -8
- package/dist/repositories/pg-errors.js.map +1 -1
- package/dist/repositories/posts.d.ts +39 -0
- package/dist/repositories/posts.js +255 -0
- package/dist/repositories/posts.js.map +1 -0
- package/dist/repositories/progress.d.ts +1 -1
- package/dist/repositories/progress.js +3 -0
- package/dist/repositories/progress.js.map +1 -1
- package/dist/repositories/settings.js +2 -0
- package/dist/repositories/settings.js.map +1 -1
- package/dist/schema/assets.d.ts +36 -2
- package/dist/schema/assets.js +12 -5
- package/dist/schema/assets.js.map +1 -1
- package/dist/schema/automations.d.ts +123 -8
- package/dist/schema/automations.js +11 -4
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/better-auth.d.ts +1613 -238
- package/dist/schema/better-auth.js +107 -22
- package/dist/schema/better-auth.js.map +1 -1
- package/dist/schema/content.d.ts +909 -22
- package/dist/schema/content.js +157 -13
- package/dist/schema/content.js.map +1 -1
- package/dist/schema/credentials.js +1 -1
- package/dist/schema/credentials.js.map +1 -1
- package/dist/schema/discussion.d.ts +2079 -55
- package/dist/schema/discussion.js +430 -13
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/entitlements.d.ts +484 -2
- package/dist/schema/entitlements.js +121 -6
- package/dist/schema/entitlements.js.map +1 -1
- package/dist/schema/identity.d.ts +517 -1
- package/dist/schema/identity.js +118 -2
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +6 -6
- package/dist/schema/index.js +32 -7
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/integrations.js +1 -1
- package/dist/schema/integrations.js.map +1 -1
- package/dist/schema/organizations.d.ts +21 -4
- package/dist/schema/organizations.js +4 -1
- package/dist/schema/organizations.js.map +1 -1
- package/dist/schema/outbox.d.ts +17 -0
- package/dist/schema/outbox.js +1 -0
- package/dist/schema/outbox.js.map +1 -1
- package/dist/schema/progress.d.ts +17 -0
- package/dist/schema/progress.js +1 -0
- package/dist/schema/progress.js.map +1 -1
- package/dist/schema/settings.js.map +1 -1
- package/dist/unit-of-work.js +7 -2
- package/dist/unit-of-work.js.map +1 -1
- package/drizzle/0000_baseline.sql +0 -1
- package/drizzle/0001_new_sentry.sql +43 -0
- package/drizzle/0002_automation_kinds.sql +7 -0
- package/drizzle/0003_community.sql +340 -0
- package/drizzle/0004_assets_sources.sql +1 -0
- package/drizzle/0005_notices_and_seed_index.sql +12 -0
- package/drizzle/0006_community_review_fixes.sql +24 -0
- package/drizzle/0007_notifications.sql +37 -0
- package/drizzle/0008_content_access.sql +13 -0
- package/drizzle/0009_community_indexes.sql +56 -0
- package/drizzle/0010_mcp_access.sql +114 -0
- package/drizzle/meta/0000_snapshot.json +167 -597
- package/drizzle/meta/0001_snapshot.json +3769 -0
- package/drizzle/meta/0002_snapshot.json +3770 -0
- package/drizzle/meta/0003_snapshot.json +7061 -0
- package/drizzle/meta/0004_snapshot.json +7068 -0
- package/drizzle/meta/0005_snapshot.json +7180 -0
- package/drizzle/meta/0006_snapshot.json +7298 -0
- package/drizzle/meta/0007_snapshot.json +7620 -0
- package/drizzle/meta/0008_snapshot.json +7666 -0
- package/drizzle/meta/0009_snapshot.json +8185 -0
- package/drizzle/meta/0010_snapshot.json +8813 -0
- package/drizzle/meta/_journal.json +71 -1
- package/package.json +3 -2
- package/dist/repositories/dashboard.d.ts +0 -14
- package/dist/repositories/dashboard.js +0 -55
- package/dist/repositories/dashboard.js.map +0 -1
- package/dist/repositories/learn.d.ts +0 -19
- package/dist/repositories/learn.js +0 -54
- package/dist/repositories/learn.js.map +0 -1
- package/dist/repositories/students.d.ts +0 -15
- package/dist/repositories/students.js +0 -83
- package/dist/repositories/students.js.map +0 -1
|
@@ -1,14 +1,63 @@
|
|
|
1
|
-
import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
|
|
2
|
-
import {
|
|
1
|
+
import { activities, activityAssets, bundleItems, bundles, communities, contentItems, courses, downloadAssets, downloads, modules, spaceLinks, spaceTopics, spaces } from "../schema/content.js";
|
|
2
|
+
import { posts } from "../schema/discussion.js";
|
|
3
|
+
import { settings } from "../schema/settings.js";
|
|
4
|
+
import { isUniqueViolation, pgErrorFields, translateDbErrors } from "./pg-errors.js";
|
|
5
|
+
import { deepMerge } from "./settings.js";
|
|
3
6
|
import { and, asc, count, desc, eq, ilike, inArray, or, sql } from "drizzle-orm";
|
|
4
7
|
import { genId } from "@headless-lms/core/shared/id";
|
|
5
|
-
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
8
|
import { ConflictError, NotFoundError } from "@headless-lms/core/shared/errors";
|
|
9
|
+
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
10
|
+
import { communitySettingsDefaults, courseSettingsDefaults, spaceSettingsDefaults } from "@headless-lms/core/schemas";
|
|
7
11
|
//#region src/repositories/content.ts
|
|
8
|
-
|
|
12
|
+
const LIBRARY_ONLY = "An activity can only use ready files from the media library";
|
|
13
|
+
const MISSING_ASSET_DETAIL = /^Key \(org_id, asset_id\)=\((?:[^,]+), (.+?)\) is not present/;
|
|
14
|
+
function refusedLink(err) {
|
|
15
|
+
const pg = pgErrorFields(err);
|
|
16
|
+
if (pg?.code !== "23503") return;
|
|
17
|
+
const missing = pg.detail?.match(MISSING_ASSET_DETAIL)?.[1];
|
|
18
|
+
const message = missing ? `${LIBRARY_ONLY}. Not available: ${missing}` : LIBRARY_ONLY;
|
|
19
|
+
return Object.assign(new ConflictError(message), { cause: err });
|
|
20
|
+
}
|
|
21
|
+
const SETTINGS_NAMESPACE = "content";
|
|
22
|
+
const courseSettingsJoin = and(eq(settings.orgId, courses.orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, courses.id));
|
|
23
|
+
const communitySettingsJoin = and(eq(settings.orgId, communities.orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, communities.id));
|
|
24
|
+
function toCommunity(row, storedSettings) {
|
|
25
|
+
const raw = storedSettings?.dm;
|
|
26
|
+
const stored = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
9
27
|
return {
|
|
10
28
|
...row,
|
|
11
|
-
status: row.status
|
|
29
|
+
status: row.status,
|
|
30
|
+
settings: { dm: {
|
|
31
|
+
...communitySettingsDefaults.dm,
|
|
32
|
+
...stored
|
|
33
|
+
} }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function toSpace(row) {
|
|
37
|
+
return {
|
|
38
|
+
...row,
|
|
39
|
+
accessRule: row.accessRule,
|
|
40
|
+
settings: {
|
|
41
|
+
...spaceSettingsDefaults,
|
|
42
|
+
...row.settings
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const communitySortColumns = {
|
|
47
|
+
title: communities.title,
|
|
48
|
+
slug: communities.slug,
|
|
49
|
+
status: communities.status,
|
|
50
|
+
createdAt: communities.createdAt,
|
|
51
|
+
updatedAt: communities.updatedAt
|
|
52
|
+
};
|
|
53
|
+
function toCourse(row, storedSettings) {
|
|
54
|
+
return {
|
|
55
|
+
...row,
|
|
56
|
+
status: row.status,
|
|
57
|
+
settings: {
|
|
58
|
+
...courseSettingsDefaults,
|
|
59
|
+
...storedSettings
|
|
60
|
+
}
|
|
12
61
|
};
|
|
13
62
|
}
|
|
14
63
|
const sortColumns = {
|
|
@@ -33,6 +82,11 @@ function toDownload(row) {
|
|
|
33
82
|
status: row.status
|
|
34
83
|
};
|
|
35
84
|
}
|
|
85
|
+
const bundleSortColumns = {
|
|
86
|
+
name: bundles.name,
|
|
87
|
+
createdAt: bundles.createdAt,
|
|
88
|
+
updatedAt: bundles.updatedAt
|
|
89
|
+
};
|
|
36
90
|
var DrizzleContentRepository = class {
|
|
37
91
|
db;
|
|
38
92
|
logger;
|
|
@@ -45,7 +99,7 @@ var DrizzleContentRepository = class {
|
|
|
45
99
|
tx(fn) {
|
|
46
100
|
return this.db.transaction(fn);
|
|
47
101
|
}
|
|
48
|
-
async
|
|
102
|
+
async listCourses(orgId, query) {
|
|
49
103
|
const conditions = [eq(courses.orgId, orgId)];
|
|
50
104
|
if (query.status) conditions.push(eq(courses.status, query.status));
|
|
51
105
|
if (query.category) conditions.push(eq(courses.category, query.category));
|
|
@@ -68,20 +122,26 @@ var DrizzleContentRepository = class {
|
|
|
68
122
|
}
|
|
69
123
|
const sortExpr = sortColumns[sortKey] ?? courses.createdAt;
|
|
70
124
|
const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
|
|
71
|
-
const rows = await this.db.select(
|
|
125
|
+
const rows = await this.db.select({
|
|
126
|
+
course: courses,
|
|
127
|
+
settingsValue: settings.value
|
|
128
|
+
}).from(courses).leftJoin(settings, courseSettingsJoin).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
72
129
|
const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);
|
|
73
130
|
return {
|
|
74
|
-
rows: rows.map(toCourse),
|
|
131
|
+
rows: rows.map((row) => toCourse(row.course, row.settingsValue)),
|
|
75
132
|
total: totalRow?.value ?? 0,
|
|
76
133
|
page: query.page,
|
|
77
134
|
pageSize: query.pageSize
|
|
78
135
|
};
|
|
79
136
|
}
|
|
80
|
-
async
|
|
81
|
-
const [row] = await this.db.select(
|
|
82
|
-
|
|
137
|
+
async findCourseById(orgId, id) {
|
|
138
|
+
const [row] = await this.db.select({
|
|
139
|
+
course: courses,
|
|
140
|
+
settingsValue: settings.value
|
|
141
|
+
}).from(courses).leftJoin(settings, courseSettingsJoin).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).limit(1);
|
|
142
|
+
return row ? toCourse(row.course, row.settingsValue) : null;
|
|
83
143
|
}
|
|
84
|
-
async
|
|
144
|
+
async createCourse(orgId, input, slug) {
|
|
85
145
|
const id = genId("course");
|
|
86
146
|
await this.db.insert(contentItems).values({
|
|
87
147
|
orgId,
|
|
@@ -97,31 +157,68 @@ var DrizzleContentRepository = class {
|
|
|
97
157
|
category: input.category ?? ""
|
|
98
158
|
}).returning({ id: courses.id });
|
|
99
159
|
if (!inserted) throw new Error("failed to insert course");
|
|
100
|
-
const created = await this.
|
|
160
|
+
const created = await this.findCourseById(orgId, inserted.id);
|
|
101
161
|
if (!created) throw new Error("failed to load created course");
|
|
102
162
|
return created;
|
|
103
163
|
}
|
|
104
|
-
async
|
|
164
|
+
async updateCourse(orgId, id, patch) {
|
|
105
165
|
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
106
166
|
if (patch.title !== void 0) set.title = patch.title;
|
|
107
167
|
if (patch.description !== void 0) set.description = patch.description;
|
|
108
168
|
if (patch.category !== void 0) set.category = patch.category;
|
|
109
169
|
if (patch.status !== void 0) set.status = patch.status;
|
|
110
170
|
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
171
|
const [updated] = await this.db.update(courses).set(set).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).returning({ id: courses.id });
|
|
113
172
|
if (!updated) return null;
|
|
114
|
-
return this.
|
|
173
|
+
return this.findCourseById(orgId, id);
|
|
174
|
+
}
|
|
175
|
+
async patchCourseSettings(orgId, id, value) {
|
|
176
|
+
const key = and(eq(settings.orgId, orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, id));
|
|
177
|
+
const merged = await this.tx(async (tx) => {
|
|
178
|
+
let [existing] = await tx.select().from(settings).where(key).for("update");
|
|
179
|
+
if (!existing) {
|
|
180
|
+
const [inserted] = await tx.insert(settings).values({
|
|
181
|
+
orgId,
|
|
182
|
+
namespace: SETTINGS_NAMESPACE,
|
|
183
|
+
scopeId: id,
|
|
184
|
+
value
|
|
185
|
+
}).onConflictDoNothing().returning();
|
|
186
|
+
if (inserted) return inserted.value;
|
|
187
|
+
[existing] = await tx.select().from(settings).where(key).for("update");
|
|
188
|
+
if (!existing) throw new Error("course settings patch: row vanished mid-transaction");
|
|
189
|
+
}
|
|
190
|
+
const [updated] = await tx.update(settings).set({
|
|
191
|
+
value: deepMerge(existing.value, value),
|
|
192
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
193
|
+
}).where(key).returning();
|
|
194
|
+
if (!updated) throw new Error("course settings patch returned no row");
|
|
195
|
+
return updated.value;
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
...courseSettingsDefaults,
|
|
199
|
+
...merged
|
|
200
|
+
};
|
|
115
201
|
}
|
|
116
|
-
async
|
|
202
|
+
async deleteCourse(orgId, id) {
|
|
117
203
|
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
204
|
}
|
|
119
|
-
|
|
205
|
+
listCourseModules(orgId, courseId) {
|
|
120
206
|
return this.tx((tx) => this.listModules(tx, orgId, courseId));
|
|
121
207
|
}
|
|
122
208
|
async listActivitiesForCourse(orgId, courseId) {
|
|
123
209
|
return this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activities.moduleId, activities.seq);
|
|
124
210
|
}
|
|
211
|
+
async listActivityAssetsForCourse(orgId, courseId) {
|
|
212
|
+
return this.db.select({
|
|
213
|
+
orgId: activityAssets.orgId,
|
|
214
|
+
id: activityAssets.id,
|
|
215
|
+
activityId: activityAssets.activityId,
|
|
216
|
+
assetId: activityAssets.assetId,
|
|
217
|
+
seq: activityAssets.seq,
|
|
218
|
+
createdAt: activityAssets.createdAt,
|
|
219
|
+
updatedAt: activityAssets.updatedAt
|
|
220
|
+
}).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);
|
|
221
|
+
}
|
|
125
222
|
async findActivity(orgId, activityId) {
|
|
126
223
|
const [row] = await this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId))).limit(1);
|
|
127
224
|
return row ?? null;
|
|
@@ -142,12 +239,16 @@ var DrizzleContentRepository = class {
|
|
|
142
239
|
/** Replace an activity's ordered asset links with `assetIds`. */
|
|
143
240
|
async replaceActivityAssets(tx, orgId, activityId, assetIds) {
|
|
144
241
|
await tx.delete(activityAssets).where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));
|
|
145
|
-
if (assetIds.length)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
242
|
+
if (assetIds.length) try {
|
|
243
|
+
await tx.insert(activityAssets).values(assetIds.map((assetId, i) => ({
|
|
244
|
+
orgId,
|
|
245
|
+
activityId,
|
|
246
|
+
assetId,
|
|
247
|
+
seq: i
|
|
248
|
+
})));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
throw refusedLink(err) ?? err;
|
|
251
|
+
}
|
|
151
252
|
}
|
|
152
253
|
createModule(orgId, courseId, title) {
|
|
153
254
|
return this.tx(async (tx) => {
|
|
@@ -343,7 +444,340 @@ var DrizzleContentRepository = class {
|
|
|
343
444
|
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
445
|
return this.listDownloadAssets(orgId, downloadId);
|
|
345
446
|
}
|
|
447
|
+
async listBundles(orgId, query) {
|
|
448
|
+
const conditions = [eq(bundles.orgId, orgId)];
|
|
449
|
+
const search = query.search?.trim();
|
|
450
|
+
if (search) conditions.push(ilike(bundles.name, `%${search}%`));
|
|
451
|
+
const where = and(...conditions);
|
|
452
|
+
let sortKey = "createdAt";
|
|
453
|
+
let direction = "desc";
|
|
454
|
+
if (query.sort) {
|
|
455
|
+
const isDesc = query.sort.startsWith("-");
|
|
456
|
+
const key = isDesc ? query.sort.slice(1) : query.sort;
|
|
457
|
+
if (key in bundleSortColumns) {
|
|
458
|
+
sortKey = key;
|
|
459
|
+
direction = isDesc ? "desc" : "asc";
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const sortExpr = bundleSortColumns[sortKey] ?? bundles.createdAt;
|
|
463
|
+
const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
|
|
464
|
+
const rows = await this.db.select().from(bundles).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
465
|
+
const [totalRow] = await this.db.select({ value: count() }).from(bundles).where(where);
|
|
466
|
+
return {
|
|
467
|
+
rows,
|
|
468
|
+
total: totalRow?.value ?? 0,
|
|
469
|
+
page: query.page,
|
|
470
|
+
pageSize: query.pageSize
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async findBundleById(orgId, id) {
|
|
474
|
+
const [row] = await this.db.select().from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).limit(1);
|
|
475
|
+
return row ?? null;
|
|
476
|
+
}
|
|
477
|
+
/** The bundle and its initial items are one write: an unknown content id
|
|
478
|
+
* rolls the bundle back rather than leaving it created and half-filled. */
|
|
479
|
+
async createBundle(orgId, input) {
|
|
480
|
+
const contentIds = [...new Set(input.contentIds ?? [])];
|
|
481
|
+
return this.tx(async (tx) => {
|
|
482
|
+
const [row] = await tx.insert(bundles).values({
|
|
483
|
+
orgId,
|
|
484
|
+
id: genId("bundle"),
|
|
485
|
+
name: input.name
|
|
486
|
+
}).returning();
|
|
487
|
+
if (!row) throw new Error("failed to load created bundle");
|
|
488
|
+
if (contentIds.length > 0) {
|
|
489
|
+
const found = await tx.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), inArray(contentItems.id, contentIds)));
|
|
490
|
+
const known = new Set(found.map((item) => item.id));
|
|
491
|
+
const missing = contentIds.find((id) => !known.has(id));
|
|
492
|
+
if (missing) throw new NotFoundError("Content", missing);
|
|
493
|
+
await tx.insert(bundleItems).values(contentIds.map((contentId) => ({
|
|
494
|
+
orgId,
|
|
495
|
+
bundleId: row.id,
|
|
496
|
+
contentId
|
|
497
|
+
})));
|
|
498
|
+
}
|
|
499
|
+
return row;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async updateBundle(orgId, id, patch) {
|
|
503
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
504
|
+
if (patch.name !== void 0) set.name = patch.name;
|
|
505
|
+
const [row] = await this.db.update(bundles).set(set).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning();
|
|
506
|
+
return row ?? null;
|
|
507
|
+
}
|
|
508
|
+
async deleteBundle(orgId, id) {
|
|
509
|
+
return (await this.db.delete(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, id))).returning({ id: bundles.id })).length > 0;
|
|
510
|
+
}
|
|
511
|
+
async listBundleItems(orgId, bundleId) {
|
|
512
|
+
return this.db.select().from(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId))).orderBy(asc(bundleItems.createdAt));
|
|
513
|
+
}
|
|
514
|
+
/** Assert the bundle exists in the org; throw otherwise. */
|
|
515
|
+
async assertBundle(orgId, bundleId) {
|
|
516
|
+
const [row] = await this.db.select({ id: bundles.id }).from(bundles).where(and(eq(bundles.orgId, orgId), eq(bundles.id, bundleId))).limit(1);
|
|
517
|
+
if (!row) throw new NotFoundError("Bundle", bundleId);
|
|
518
|
+
}
|
|
519
|
+
async addBundleItem(orgId, bundleId, contentId) {
|
|
520
|
+
await this.assertBundle(orgId, bundleId);
|
|
521
|
+
const [item] = await this.db.select({ id: contentItems.id }).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
|
|
522
|
+
if (!item) throw new NotFoundError("Content", contentId);
|
|
523
|
+
await this.db.insert(bundleItems).values({
|
|
524
|
+
orgId,
|
|
525
|
+
bundleId,
|
|
526
|
+
contentId
|
|
527
|
+
}).onConflictDoNothing({ target: [
|
|
528
|
+
bundleItems.orgId,
|
|
529
|
+
bundleItems.bundleId,
|
|
530
|
+
bundleItems.contentId
|
|
531
|
+
] });
|
|
532
|
+
return this.listBundleItems(orgId, bundleId);
|
|
533
|
+
}
|
|
534
|
+
async removeBundleItem(orgId, bundleId, contentId) {
|
|
535
|
+
await this.assertBundle(orgId, bundleId);
|
|
536
|
+
await this.db.delete(bundleItems).where(and(eq(bundleItems.orgId, orgId), eq(bundleItems.bundleId, bundleId), eq(bundleItems.contentId, contentId)));
|
|
537
|
+
return this.listBundleItems(orgId, bundleId);
|
|
538
|
+
}
|
|
539
|
+
async listCommunities(orgId, query) {
|
|
540
|
+
const conditions = [eq(communities.orgId, orgId)];
|
|
541
|
+
if (query.status) conditions.push(eq(communities.status, query.status));
|
|
542
|
+
const search = query.search?.trim();
|
|
543
|
+
if (search) {
|
|
544
|
+
const like = `%${search}%`;
|
|
545
|
+
const match = or(ilike(communities.title, like), ilike(communities.description, like));
|
|
546
|
+
if (match) conditions.push(match);
|
|
547
|
+
}
|
|
548
|
+
const where = and(...conditions);
|
|
549
|
+
let sortKey = "createdAt";
|
|
550
|
+
let direction = "desc";
|
|
551
|
+
if (query.sort) {
|
|
552
|
+
const isDesc = query.sort.startsWith("-");
|
|
553
|
+
const key = isDesc ? query.sort.slice(1) : query.sort;
|
|
554
|
+
if (key in communitySortColumns) {
|
|
555
|
+
sortKey = key;
|
|
556
|
+
direction = isDesc ? "desc" : "asc";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const sortExpr = communitySortColumns[sortKey] ?? communities.createdAt;
|
|
560
|
+
const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
|
|
561
|
+
const rows = await this.db.select({
|
|
562
|
+
community: communities,
|
|
563
|
+
settingsValue: settings.value
|
|
564
|
+
}).from(communities).leftJoin(settings, communitySettingsJoin).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
565
|
+
const [totalRow] = await this.db.select({ value: count() }).from(communities).where(where);
|
|
566
|
+
return {
|
|
567
|
+
rows: rows.map((row) => toCommunity(row.community, row.settingsValue)),
|
|
568
|
+
total: totalRow?.value ?? 0,
|
|
569
|
+
page: query.page,
|
|
570
|
+
pageSize: query.pageSize
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async findCommunityById(orgId, id) {
|
|
574
|
+
const [row] = await this.db.select({
|
|
575
|
+
community: communities,
|
|
576
|
+
settingsValue: settings.value
|
|
577
|
+
}).from(communities).leftJoin(settings, communitySettingsJoin).where(and(eq(communities.orgId, orgId), eq(communities.id, id))).limit(1);
|
|
578
|
+
return row ? toCommunity(row.community, row.settingsValue) : null;
|
|
579
|
+
}
|
|
580
|
+
async findCommunitiesByIds(orgId, ids) {
|
|
581
|
+
return (await this.db.select({
|
|
582
|
+
community: communities,
|
|
583
|
+
settingsValue: settings.value
|
|
584
|
+
}).from(communities).leftJoin(settings, communitySettingsJoin).where(and(eq(communities.orgId, orgId), inArray(communities.id, ids))).orderBy(asc(communities.title), asc(communities.id))).map((row) => toCommunity(row.community, row.settingsValue));
|
|
585
|
+
}
|
|
586
|
+
async listPublishedCommunityIds(orgId) {
|
|
587
|
+
return (await this.db.select({ id: communities.id }).from(communities).where(and(eq(communities.orgId, orgId), eq(communities.status, "published"))).orderBy(asc(communities.createdAt), asc(communities.id))).map((row) => row.id);
|
|
588
|
+
}
|
|
589
|
+
async createCommunity(orgId, input, slug) {
|
|
590
|
+
const id = genId("community");
|
|
591
|
+
await this.db.insert(contentItems).values({
|
|
592
|
+
orgId,
|
|
593
|
+
id,
|
|
594
|
+
type: "community"
|
|
595
|
+
});
|
|
596
|
+
const [inserted] = await this.db.insert(communities).values({
|
|
597
|
+
orgId,
|
|
598
|
+
id,
|
|
599
|
+
title: input.title,
|
|
600
|
+
slug,
|
|
601
|
+
description: input.description ?? ""
|
|
602
|
+
}).returning({ id: communities.id });
|
|
603
|
+
if (!inserted) throw new Error("failed to insert community");
|
|
604
|
+
const created = await this.findCommunityById(orgId, inserted.id);
|
|
605
|
+
if (!created) throw new Error("failed to load created community");
|
|
606
|
+
return created;
|
|
607
|
+
}
|
|
608
|
+
async updateCommunity(orgId, id, patch) {
|
|
609
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
610
|
+
if (patch.title !== void 0) set.title = patch.title;
|
|
611
|
+
if (patch.description !== void 0) set.description = patch.description;
|
|
612
|
+
if (patch.status !== void 0) set.status = patch.status;
|
|
613
|
+
if (patch.thumbnailAssetId !== void 0) set.thumbnailAssetId = patch.thumbnailAssetId;
|
|
614
|
+
const [updated] = await this.db.update(communities).set(set).where(and(eq(communities.orgId, orgId), eq(communities.id, id))).returning({ id: communities.id });
|
|
615
|
+
if (!updated) return null;
|
|
616
|
+
return this.findCommunityById(orgId, id);
|
|
617
|
+
}
|
|
618
|
+
async patchDmSettings(orgId, id, patch) {
|
|
619
|
+
if (!await this.findCommunityById(orgId, id)) return null;
|
|
620
|
+
const key = and(eq(settings.orgId, orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, id));
|
|
621
|
+
await this.tx(async (tx) => {
|
|
622
|
+
let [existing] = await tx.select().from(settings).where(key).for("update");
|
|
623
|
+
const value = { dm: patch };
|
|
624
|
+
if (!existing) {
|
|
625
|
+
const [inserted] = await tx.insert(settings).values({
|
|
626
|
+
orgId,
|
|
627
|
+
namespace: SETTINGS_NAMESPACE,
|
|
628
|
+
scopeId: id,
|
|
629
|
+
value
|
|
630
|
+
}).onConflictDoNothing().returning();
|
|
631
|
+
if (inserted) return;
|
|
632
|
+
[existing] = await tx.select().from(settings).where(key).for("update");
|
|
633
|
+
if (!existing) throw new Error("community settings patch: row vanished mid-transaction");
|
|
634
|
+
}
|
|
635
|
+
await tx.update(settings).set({
|
|
636
|
+
value: deepMerge(existing.value, { dm: patch }),
|
|
637
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
638
|
+
}).where(key);
|
|
639
|
+
});
|
|
640
|
+
return this.findCommunityById(orgId, id);
|
|
641
|
+
}
|
|
642
|
+
async deleteCommunity(orgId, id) {
|
|
643
|
+
return (await this.db.delete(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, "community"))).returning({ id: contentItems.id })).length > 0;
|
|
644
|
+
}
|
|
645
|
+
async deleteCommunitySettings(orgId, id) {
|
|
646
|
+
await this.db.delete(settings).where(and(eq(settings.orgId, orgId), eq(settings.namespace, SETTINGS_NAMESPACE), eq(settings.scopeId, id)));
|
|
647
|
+
}
|
|
648
|
+
async listSpaces(orgId, communityId) {
|
|
649
|
+
const rows = await this.db.select().from(spaces).where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId))).orderBy(asc(spaces.seq));
|
|
650
|
+
return this.withSpaceParts(orgId, rows.map(toSpace));
|
|
651
|
+
}
|
|
652
|
+
async findSpace(orgId, spaceId) {
|
|
653
|
+
const [row] = await this.db.select().from(spaces).where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId))).limit(1);
|
|
654
|
+
if (!row) return null;
|
|
655
|
+
const [detail] = await this.withSpaceParts(orgId, [toSpace(row)]);
|
|
656
|
+
return detail ?? null;
|
|
657
|
+
}
|
|
658
|
+
async lockSpace(orgId, spaceId) {
|
|
659
|
+
const [row] = await this.db.select().from(spaces).where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId))).for("update").limit(1);
|
|
660
|
+
return row ? toSpace(row) : null;
|
|
661
|
+
}
|
|
662
|
+
async createSpace(orgId, communityId, input) {
|
|
663
|
+
const nextSeq = (await this.db.select({ seq: spaces.seq }).from(spaces).where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId)))).reduce((max, row) => Math.max(max, row.seq), -1) + 1;
|
|
664
|
+
const [inserted] = await this.db.insert(spaces).values({
|
|
665
|
+
orgId,
|
|
666
|
+
communityId,
|
|
667
|
+
name: input.name,
|
|
668
|
+
description: input.description ?? "",
|
|
669
|
+
seq: nextSeq,
|
|
670
|
+
accessRule: input.accessRule ?? "all_members",
|
|
671
|
+
capacity: input.capacity ?? null,
|
|
672
|
+
settings: {
|
|
673
|
+
...spaceSettingsDefaults,
|
|
674
|
+
...input.settings
|
|
675
|
+
}
|
|
676
|
+
}).returning({ id: spaces.id });
|
|
677
|
+
if (!inserted) throw new Error("failed to insert space");
|
|
678
|
+
const created = await this.findSpace(orgId, inserted.id);
|
|
679
|
+
if (!created) throw new Error("failed to load created space");
|
|
680
|
+
return created;
|
|
681
|
+
}
|
|
682
|
+
async updateSpace(orgId, communityId, spaceId, patch) {
|
|
683
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
684
|
+
if (patch.name !== void 0) set.name = patch.name;
|
|
685
|
+
if (patch.description !== void 0) set.description = patch.description;
|
|
686
|
+
if (patch.accessRule !== void 0) set.accessRule = patch.accessRule;
|
|
687
|
+
if (patch.capacity !== void 0) set.capacity = patch.capacity;
|
|
688
|
+
if (patch.settings !== void 0) set.settings = {
|
|
689
|
+
...spaceSettingsDefaults,
|
|
690
|
+
...patch.settings
|
|
691
|
+
};
|
|
692
|
+
const [updated] = await this.db.update(spaces).set(set).where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId), eq(spaces.communityId, communityId))).returning({ id: spaces.id });
|
|
693
|
+
if (!updated) return null;
|
|
694
|
+
return this.findSpace(orgId, spaceId);
|
|
695
|
+
}
|
|
696
|
+
async deleteSpace(orgId, communityId, spaceId) {
|
|
697
|
+
return (await this.db.delete(spaces).where(and(eq(spaces.orgId, orgId), eq(spaces.id, spaceId), eq(spaces.communityId, communityId))).returning({ id: spaces.id })).length > 0;
|
|
698
|
+
}
|
|
699
|
+
async reorderSpaces(orgId, communityId, orderedIds) {
|
|
700
|
+
for (let i = 0; i < orderedIds.length; i++) await this.db.update(spaces).set({ seq: i }).where(and(eq(spaces.orgId, orgId), eq(spaces.communityId, communityId), eq(spaces.id, orderedIds[i])));
|
|
701
|
+
return this.listSpaces(orgId, communityId);
|
|
702
|
+
}
|
|
703
|
+
async setSpaceTopics(orgId, spaceId, input) {
|
|
704
|
+
await this.tx(async (tx) => {
|
|
705
|
+
const existing = await tx.select({ id: spaceTopics.id }).from(spaceTopics).where(and(eq(spaceTopics.orgId, orgId), eq(spaceTopics.spaceId, spaceId))).for("update");
|
|
706
|
+
const kept = new Set(input.topics.flatMap((topic) => topic.id ? [topic.id] : []));
|
|
707
|
+
const known = new Set(existing.map((topic) => topic.id));
|
|
708
|
+
for (const id of kept) if (!known.has(id)) throw new NotFoundError("Topic", id);
|
|
709
|
+
const removed = existing.filter((topic) => !kept.has(topic.id)).map((topic) => topic.id);
|
|
710
|
+
if (removed.length) await tx.delete(spaceTopics).where(and(eq(spaceTopics.orgId, orgId), inArray(spaceTopics.id, removed)));
|
|
711
|
+
for (let seq = 0; seq < input.topics.length; seq++) {
|
|
712
|
+
const topic = input.topics[seq];
|
|
713
|
+
if (topic.id) await tx.update(spaceTopics).set({
|
|
714
|
+
name: topic.name,
|
|
715
|
+
seq,
|
|
716
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
717
|
+
}).where(and(eq(spaceTopics.orgId, orgId), eq(spaceTopics.id, topic.id)));
|
|
718
|
+
else await tx.insert(spaceTopics).values({
|
|
719
|
+
orgId,
|
|
720
|
+
spaceId,
|
|
721
|
+
name: topic.name,
|
|
722
|
+
seq
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
const space = await this.findSpace(orgId, spaceId);
|
|
727
|
+
if (!space) throw new NotFoundError("Space", spaceId);
|
|
728
|
+
return space;
|
|
729
|
+
}
|
|
730
|
+
async topicsInUse(orgId, spaceId, topicIds) {
|
|
731
|
+
if (topicIds.length === 0) return [];
|
|
732
|
+
await this.db.select({ id: spaceTopics.id }).from(spaceTopics).where(and(eq(spaceTopics.orgId, orgId), eq(spaceTopics.spaceId, spaceId), inArray(spaceTopics.id, topicIds))).for("update");
|
|
733
|
+
return (await this.db.selectDistinct({ topicId: posts.topicId }).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), inArray(posts.topicId, topicIds)))).flatMap((row) => row.topicId ? [row.topicId] : []);
|
|
734
|
+
}
|
|
735
|
+
async addSpaceLink(orgId, spaceId, contentId) {
|
|
736
|
+
const [item] = await this.db.select({
|
|
737
|
+
id: contentItems.id,
|
|
738
|
+
type: contentItems.type
|
|
739
|
+
}).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
|
|
740
|
+
if (!item || item.type !== "course" && item.type !== "download" && item.type !== "community") throw new NotFoundError("Content", contentId);
|
|
741
|
+
const nextSeq = (await this.db.select({ seq: spaceLinks.seq }).from(spaceLinks).where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.spaceId, spaceId)))).reduce((max, row) => Math.max(max, row.seq), -1) + 1;
|
|
742
|
+
await this.db.insert(spaceLinks).values({
|
|
743
|
+
orgId,
|
|
744
|
+
spaceId,
|
|
745
|
+
contentId,
|
|
746
|
+
seq: nextSeq
|
|
747
|
+
});
|
|
748
|
+
const space = await this.findSpace(orgId, spaceId);
|
|
749
|
+
if (!space) throw new NotFoundError("Space", spaceId);
|
|
750
|
+
return space;
|
|
751
|
+
}
|
|
752
|
+
async removeSpaceLink(orgId, spaceId, linkId) {
|
|
753
|
+
if ((await this.db.delete(spaceLinks).where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.spaceId, spaceId), eq(spaceLinks.id, linkId))).returning({ id: spaceLinks.id })).length === 0) throw new NotFoundError("Link", linkId);
|
|
754
|
+
const space = await this.findSpace(orgId, spaceId);
|
|
755
|
+
if (!space) throw new NotFoundError("Space", spaceId);
|
|
756
|
+
return space;
|
|
757
|
+
}
|
|
758
|
+
async reorderSpaceLinks(orgId, spaceId, orderedIds) {
|
|
759
|
+
for (let i = 0; i < orderedIds.length; i++) await this.db.update(spaceLinks).set({ seq: i }).where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.spaceId, spaceId), eq(spaceLinks.id, orderedIds[i])));
|
|
760
|
+
const space = await this.findSpace(orgId, spaceId);
|
|
761
|
+
if (!space) throw new NotFoundError("Space", spaceId);
|
|
762
|
+
return space;
|
|
763
|
+
}
|
|
764
|
+
async spacesLinkingContent(orgId, contentId) {
|
|
765
|
+
const rows = await this.db.select({ spaceId: spaceLinks.spaceId }).from(spaceLinks).where(and(eq(spaceLinks.orgId, orgId), eq(spaceLinks.contentId, contentId)));
|
|
766
|
+
return [...new Set(rows.map((row) => row.spaceId))];
|
|
767
|
+
}
|
|
768
|
+
async withSpaceParts(orgId, rows) {
|
|
769
|
+
if (rows.length === 0) return [];
|
|
770
|
+
const ids = rows.map((row) => row.id);
|
|
771
|
+
const topics = await this.db.select().from(spaceTopics).where(and(eq(spaceTopics.orgId, orgId), inArray(spaceTopics.spaceId, ids))).orderBy(asc(spaceTopics.seq));
|
|
772
|
+
const links = await this.db.select().from(spaceLinks).where(and(eq(spaceLinks.orgId, orgId), inArray(spaceLinks.spaceId, ids))).orderBy(asc(spaceLinks.seq));
|
|
773
|
+
return rows.map((space) => ({
|
|
774
|
+
...space,
|
|
775
|
+
topics: topics.filter((topic) => topic.spaceId === space.id),
|
|
776
|
+
links: links.filter((link) => link.spaceId === space.id)
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
346
779
|
};
|
|
780
|
+
translateDbErrors(DrizzleContentRepository);
|
|
347
781
|
//#endregion
|
|
348
782
|
export { DrizzleContentRepository };
|
|
349
783
|
|