@headless-lms/adapter-db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/client.d.ts +17 -0
  4. package/dist/client.js +11 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/index.d.ts +22 -0
  7. package/dist/index.js +22 -0
  8. package/dist/migrate.d.ts +7 -0
  9. package/dist/migrate.js +27 -0
  10. package/dist/migrate.js.map +1 -0
  11. package/dist/repositories/assets.d.ts +17 -0
  12. package/dist/repositories/assets.js +90 -0
  13. package/dist/repositories/assets.js.map +1 -0
  14. package/dist/repositories/automations.d.ts +30 -0
  15. package/dist/repositories/automations.js +165 -0
  16. package/dist/repositories/automations.js.map +1 -0
  17. package/dist/repositories/content.d.ts +52 -0
  18. package/dist/repositories/content.js +350 -0
  19. package/dist/repositories/content.js.map +1 -0
  20. package/dist/repositories/credentials.d.ts +25 -0
  21. package/dist/repositories/credentials.js +86 -0
  22. package/dist/repositories/credentials.js.map +1 -0
  23. package/dist/repositories/dashboard.d.ts +14 -0
  24. package/dist/repositories/dashboard.js +55 -0
  25. package/dist/repositories/dashboard.js.map +1 -0
  26. package/dist/repositories/discussion.d.ts +31 -0
  27. package/dist/repositories/discussion.js +201 -0
  28. package/dist/repositories/discussion.js.map +1 -0
  29. package/dist/repositories/entitlements.d.ts +23 -0
  30. package/dist/repositories/entitlements.js +129 -0
  31. package/dist/repositories/entitlements.js.map +1 -0
  32. package/dist/repositories/identity.d.ts +17 -0
  33. package/dist/repositories/identity.js +44 -0
  34. package/dist/repositories/identity.js.map +1 -0
  35. package/dist/repositories/integrations.d.ts +18 -0
  36. package/dist/repositories/integrations.js +65 -0
  37. package/dist/repositories/integrations.js.map +1 -0
  38. package/dist/repositories/learn.d.ts +19 -0
  39. package/dist/repositories/learn.js +54 -0
  40. package/dist/repositories/learn.js.map +1 -0
  41. package/dist/repositories/members.d.ts +16 -0
  42. package/dist/repositories/members.js +116 -0
  43. package/dist/repositories/members.js.map +1 -0
  44. package/dist/repositories/org-user-profile.d.ts +92 -0
  45. package/dist/repositories/org-user-profile.js +16 -0
  46. package/dist/repositories/org-user-profile.js.map +1 -0
  47. package/dist/repositories/organizations.d.ts +33 -0
  48. package/dist/repositories/organizations.js +198 -0
  49. package/dist/repositories/organizations.js.map +1 -0
  50. package/dist/repositories/outbox.d.ts +24 -0
  51. package/dist/repositories/outbox.js +56 -0
  52. package/dist/repositories/outbox.js.map +1 -0
  53. package/dist/repositories/pg-errors.d.ts +11 -0
  54. package/dist/repositories/pg-errors.js +15 -0
  55. package/dist/repositories/pg-errors.js.map +1 -0
  56. package/dist/repositories/progress.d.ts +18 -0
  57. package/dist/repositories/progress.js +59 -0
  58. package/dist/repositories/progress.js.map +1 -0
  59. package/dist/repositories/settings.d.ts +22 -0
  60. package/dist/repositories/settings.js +73 -0
  61. package/dist/repositories/settings.js.map +1 -0
  62. package/dist/repositories/students.d.ts +15 -0
  63. package/dist/repositories/students.js +83 -0
  64. package/dist/repositories/students.js.map +1 -0
  65. package/dist/schema/assets.d.ts +181 -0
  66. package/dist/schema/assets.js +24 -0
  67. package/dist/schema/assets.js.map +1 -0
  68. package/dist/schema/automations.d.ts +364 -0
  69. package/dist/schema/automations.js +42 -0
  70. package/dist/schema/automations.js.map +1 -0
  71. package/dist/schema/better-auth.d.ts +2311 -0
  72. package/dist/schema/better-auth.js +156 -0
  73. package/dist/schema/better-auth.js.map +1 -0
  74. package/dist/schema/content.d.ts +966 -0
  75. package/dist/schema/content.js +163 -0
  76. package/dist/schema/content.js.map +1 -0
  77. package/dist/schema/credentials.d.ts +113 -0
  78. package/dist/schema/credentials.js +15 -0
  79. package/dist/schema/credentials.js.map +1 -0
  80. package/dist/schema/discussion.d.ts +399 -0
  81. package/dist/schema/discussion.js +89 -0
  82. package/dist/schema/discussion.js.map +1 -0
  83. package/dist/schema/drift.d.ts +9 -0
  84. package/dist/schema/drift.js +1 -0
  85. package/dist/schema/entitlements.d.ts +147 -0
  86. package/dist/schema/entitlements.js +30 -0
  87. package/dist/schema/entitlements.js.map +1 -0
  88. package/dist/schema/identity.d.ts +130 -0
  89. package/dist/schema/identity.js +16 -0
  90. package/dist/schema/identity.js.map +1 -0
  91. package/dist/schema/index.d.ts +18 -0
  92. package/dist/schema/index.js +43 -0
  93. package/dist/schema/index.js.map +1 -0
  94. package/dist/schema/integrations.d.ts +149 -0
  95. package/dist/schema/integrations.js +25 -0
  96. package/dist/schema/integrations.js.map +1 -0
  97. package/dist/schema/organizations.d.ts +416 -0
  98. package/dist/schema/organizations.js +58 -0
  99. package/dist/schema/organizations.js.map +1 -0
  100. package/dist/schema/outbox.d.ts +166 -0
  101. package/dist/schema/outbox.js +19 -0
  102. package/dist/schema/outbox.js.map +1 -0
  103. package/dist/schema/progress.d.ts +166 -0
  104. package/dist/schema/progress.js +30 -0
  105. package/dist/schema/progress.js.map +1 -0
  106. package/dist/schema/settings.d.ts +115 -0
  107. package/dist/schema/settings.js +19 -0
  108. package/dist/schema/settings.js.map +1 -0
  109. package/dist/unit-of-work.d.ts +13 -0
  110. package/dist/unit-of-work.js +16 -0
  111. package/dist/unit-of-work.js.map +1 -0
  112. package/drizzle/0000_baseline.sql +526 -0
  113. package/drizzle/meta/0000_snapshot.json +3925 -0
  114. package/drizzle/meta/_journal.json +13 -0
  115. package/package.json +56 -0
@@ -0,0 +1,350 @@
1
+ import { activities, activityAssets, contentItems, courses, downloadAssets, downloads, modules } from "../schema/content.js";
2
+ import { isUniqueViolation } from "./pg-errors.js";
3
+ import { and, asc, count, desc, eq, ilike, inArray, or, sql } from "drizzle-orm";
4
+ import { genId } from "@headless-lms/core/shared/id";
5
+ import { noopLogger } from "@headless-lms/core/shared/logger";
6
+ import { ConflictError, NotFoundError } from "@headless-lms/core/shared/errors";
7
+ //#region src/repositories/content.ts
8
+ function toCourse(row) {
9
+ return {
10
+ ...row,
11
+ status: row.status
12
+ };
13
+ }
14
+ const sortColumns = {
15
+ title: courses.title,
16
+ slug: courses.slug,
17
+ status: courses.status,
18
+ category: courses.category,
19
+ createdAt: courses.createdAt,
20
+ updatedAt: courses.updatedAt
21
+ };
22
+ const downloadSortColumns = {
23
+ title: downloads.title,
24
+ slug: downloads.slug,
25
+ status: downloads.status,
26
+ category: downloads.category,
27
+ createdAt: downloads.createdAt,
28
+ updatedAt: downloads.updatedAt
29
+ };
30
+ function toDownload(row) {
31
+ return {
32
+ ...row,
33
+ status: row.status
34
+ };
35
+ }
36
+ var DrizzleContentRepository = class {
37
+ db;
38
+ logger;
39
+ constructor(db, logger = noopLogger) {
40
+ this.db = db;
41
+ this.logger = logger;
42
+ }
43
+ /** On the root db this opens a transaction; tx-bound (ContentUnitOfWork) it
44
+ * nests as a savepoint inside the surrounding transaction. */
45
+ tx(fn) {
46
+ return this.db.transaction(fn);
47
+ }
48
+ async list(orgId, query) {
49
+ const conditions = [eq(courses.orgId, orgId)];
50
+ if (query.status) conditions.push(eq(courses.status, query.status));
51
+ if (query.category) conditions.push(eq(courses.category, query.category));
52
+ const search = query.search?.trim();
53
+ if (search) {
54
+ const like = `%${search}%`;
55
+ const match = or(ilike(courses.title, like), ilike(courses.category, like));
56
+ if (match) conditions.push(match);
57
+ }
58
+ const where = and(...conditions);
59
+ let sortKey = "createdAt";
60
+ let direction = "desc";
61
+ if (query.sort) {
62
+ const isDesc = query.sort.startsWith("-");
63
+ const key = isDesc ? query.sort.slice(1) : query.sort;
64
+ if (key in sortColumns) {
65
+ sortKey = key;
66
+ direction = isDesc ? "desc" : "asc";
67
+ }
68
+ }
69
+ const sortExpr = sortColumns[sortKey] ?? courses.createdAt;
70
+ const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
71
+ const rows = await this.db.select().from(courses).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
72
+ const [totalRow] = await this.db.select({ value: count() }).from(courses).where(where);
73
+ return {
74
+ rows: rows.map(toCourse),
75
+ total: totalRow?.value ?? 0,
76
+ page: query.page,
77
+ pageSize: query.pageSize
78
+ };
79
+ }
80
+ async findById(orgId, id) {
81
+ const [row] = await this.db.select().from(courses).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).limit(1);
82
+ return row ? toCourse(row) : null;
83
+ }
84
+ async create(orgId, input, slug) {
85
+ const id = genId("course");
86
+ await this.db.insert(contentItems).values({
87
+ orgId,
88
+ id,
89
+ type: "course"
90
+ });
91
+ const [inserted] = await this.db.insert(courses).values({
92
+ orgId,
93
+ id,
94
+ title: input.title,
95
+ slug,
96
+ description: input.description ?? "",
97
+ category: input.category ?? ""
98
+ }).returning({ id: courses.id });
99
+ if (!inserted) throw new Error("failed to insert course");
100
+ const created = await this.findById(orgId, inserted.id);
101
+ if (!created) throw new Error("failed to load created course");
102
+ return created;
103
+ }
104
+ async update(orgId, id, patch) {
105
+ const set = { updatedAt: /* @__PURE__ */ new Date() };
106
+ if (patch.title !== void 0) set.title = patch.title;
107
+ if (patch.description !== void 0) set.description = patch.description;
108
+ if (patch.category !== void 0) set.category = patch.category;
109
+ if (patch.status !== void 0) set.status = patch.status;
110
+ 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
+ const [updated] = await this.db.update(courses).set(set).where(and(eq(courses.orgId, orgId), eq(courses.id, id))).returning({ id: courses.id });
113
+ if (!updated) return null;
114
+ return this.findById(orgId, id);
115
+ }
116
+ async delete(orgId, id) {
117
+ 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
+ }
119
+ listForCourse(orgId, courseId) {
120
+ return this.tx((tx) => this.listModules(tx, orgId, courseId));
121
+ }
122
+ async listActivitiesForCourse(orgId, courseId) {
123
+ return this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.courseId, courseId))).orderBy(activities.moduleId, activities.seq);
124
+ }
125
+ async findActivity(orgId, activityId) {
126
+ const [row] = await this.db.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId))).limit(1);
127
+ return row ?? null;
128
+ }
129
+ async findModule(orgId, moduleId) {
130
+ const [row] = await this.db.select().from(modules).where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId))).limit(1);
131
+ return row ?? null;
132
+ }
133
+ /** The course's full ordered module list. */
134
+ async listModules(tx, orgId, courseId) {
135
+ return tx.select().from(modules).where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId))).orderBy(modules.seq);
136
+ }
137
+ /** Assert the module belongs to the org + course; throw otherwise. */
138
+ async assertModule(tx, orgId, courseId, moduleId) {
139
+ const [row] = await tx.select({ id: modules.id }).from(modules).where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId))).limit(1);
140
+ if (!row) throw new NotFoundError("Module", moduleId);
141
+ }
142
+ /** Replace an activity's ordered asset links with `assetIds`. */
143
+ async replaceActivityAssets(tx, orgId, activityId, assetIds) {
144
+ await tx.delete(activityAssets).where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));
145
+ if (assetIds.length) await tx.insert(activityAssets).values(assetIds.map((assetId, i) => ({
146
+ orgId,
147
+ activityId,
148
+ assetId,
149
+ seq: i
150
+ })));
151
+ }
152
+ createModule(orgId, courseId, title) {
153
+ return this.tx(async (tx) => {
154
+ const nextSeq = (await tx.select({ seq: modules.seq }).from(modules).where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId)))).reduce((max, r) => Math.max(max, r.seq), -1) + 1;
155
+ await tx.insert(modules).values({
156
+ orgId,
157
+ courseId,
158
+ title,
159
+ seq: nextSeq
160
+ });
161
+ return this.listModules(tx, orgId, courseId);
162
+ });
163
+ }
164
+ updateModule(orgId, courseId, moduleId, title) {
165
+ return this.tx(async (tx) => {
166
+ await this.assertModule(tx, orgId, courseId, moduleId);
167
+ await tx.update(modules).set({ title }).where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)));
168
+ return this.listModules(tx, orgId, courseId);
169
+ });
170
+ }
171
+ deleteModule(orgId, courseId, moduleId) {
172
+ return this.tx(async (tx) => {
173
+ await this.assertModule(tx, orgId, courseId, moduleId);
174
+ const activityIds = (await tx.select({ id: activities.id }).from(activities).where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)))).map((a) => a.id);
175
+ if (activityIds.length) {
176
+ await tx.delete(activityAssets).where(and(eq(activityAssets.orgId, orgId), inArray(activityAssets.activityId, activityIds)));
177
+ await tx.delete(activities).where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)));
178
+ }
179
+ await tx.delete(modules).where(and(eq(modules.orgId, orgId), eq(modules.id, moduleId), eq(modules.courseId, courseId)));
180
+ return this.listModules(tx, orgId, courseId);
181
+ });
182
+ }
183
+ reorderModules(orgId, courseId, orderedIds) {
184
+ return this.tx(async (tx) => {
185
+ for (let i = 0; i < orderedIds.length; i++) await tx.update(modules).set({ seq: i }).where(and(eq(modules.orgId, orgId), eq(modules.courseId, courseId), eq(modules.id, orderedIds[i])));
186
+ return this.listModules(tx, orgId, courseId);
187
+ });
188
+ }
189
+ reorderActivities(orgId, courseId, moduleId, orderedIds) {
190
+ return this.tx(async (tx) => {
191
+ await this.assertModule(tx, orgId, courseId, moduleId);
192
+ for (let i = 0; i < orderedIds.length; i++) await tx.update(activities).set({ seq: -(i + 1) }).where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId), eq(activities.id, orderedIds[i])));
193
+ for (let i = 0; i < orderedIds.length; i++) await tx.update(activities).set({ seq: i }).where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId), eq(activities.id, orderedIds[i])));
194
+ return this.listModules(tx, orgId, courseId);
195
+ });
196
+ }
197
+ saveActivity(orgId, courseId, moduleId, input, activityId) {
198
+ return this.tx(async (tx) => {
199
+ await this.assertModule(tx, orgId, courseId, moduleId);
200
+ let savedId;
201
+ if (activityId) {
202
+ const [existing] = await tx.select({ id: activities.id }).from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId), eq(activities.moduleId, moduleId))).limit(1);
203
+ if (!existing) throw new NotFoundError("Activity", activityId);
204
+ await tx.update(activities).set({ settings: input.settings ?? null }).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId)));
205
+ if (input.assetIds !== void 0) await this.replaceActivityAssets(tx, orgId, activityId, input.assetIds);
206
+ savedId = activityId;
207
+ } else {
208
+ const nextSeq = (await tx.select({ seq: activities.seq }).from(activities).where(and(eq(activities.orgId, orgId), eq(activities.moduleId, moduleId)))).reduce((max, r) => Math.max(max, r.seq), -1) + 1;
209
+ const [ins] = await tx.insert(activities).values({
210
+ orgId,
211
+ moduleId,
212
+ courseId,
213
+ seq: nextSeq,
214
+ settings: input.settings ?? null
215
+ }).returning({ id: activities.id });
216
+ if (!ins) throw new Error("failed to insert activity");
217
+ await this.replaceActivityAssets(tx, orgId, ins.id, input.assetIds ?? []);
218
+ savedId = ins.id;
219
+ }
220
+ const [activity] = await tx.select().from(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, savedId))).limit(1);
221
+ if (!activity) throw new Error("failed to load saved activity");
222
+ return {
223
+ modules: await this.listModules(tx, orgId, courseId),
224
+ activity
225
+ };
226
+ });
227
+ }
228
+ deleteActivity(orgId, courseId, moduleId, activityId) {
229
+ return this.tx(async (tx) => {
230
+ await this.assertModule(tx, orgId, courseId, moduleId);
231
+ await tx.delete(activityAssets).where(and(eq(activityAssets.orgId, orgId), eq(activityAssets.activityId, activityId)));
232
+ await tx.delete(activities).where(and(eq(activities.orgId, orgId), eq(activities.id, activityId), eq(activities.moduleId, moduleId)));
233
+ return this.listModules(tx, orgId, courseId);
234
+ });
235
+ }
236
+ async listDownloads(orgId, query) {
237
+ const conditions = [eq(downloads.orgId, orgId)];
238
+ if (query.status) conditions.push(eq(downloads.status, query.status));
239
+ if (query.category) conditions.push(eq(downloads.category, query.category));
240
+ const search = query.search?.trim();
241
+ if (search) {
242
+ const like = `%${search}%`;
243
+ const match = or(ilike(downloads.title, like), ilike(downloads.category, like));
244
+ if (match) conditions.push(match);
245
+ }
246
+ const where = and(...conditions);
247
+ let sortKey = "createdAt";
248
+ let direction = "desc";
249
+ if (query.sort) {
250
+ const isDesc = query.sort.startsWith("-");
251
+ const key = isDesc ? query.sort.slice(1) : query.sort;
252
+ if (key in downloadSortColumns) {
253
+ sortKey = key;
254
+ direction = isDesc ? "desc" : "asc";
255
+ }
256
+ }
257
+ const sortExpr = downloadSortColumns[sortKey] ?? downloads.createdAt;
258
+ const orderBy = direction === "desc" ? desc(sortExpr) : asc(sortExpr);
259
+ const rows = await this.db.select().from(downloads).where(where).orderBy(orderBy).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
260
+ const [totalRow] = await this.db.select({ value: count() }).from(downloads).where(where);
261
+ return {
262
+ rows: rows.map(toDownload),
263
+ total: totalRow?.value ?? 0,
264
+ page: query.page,
265
+ pageSize: query.pageSize
266
+ };
267
+ }
268
+ async getDownload(orgId, id) {
269
+ const [row] = await this.db.select().from(downloads).where(and(eq(downloads.orgId, orgId), eq(downloads.id, id))).limit(1);
270
+ return row ? toDownload(row) : null;
271
+ }
272
+ async createDownload(orgId, input, slug) {
273
+ const id = genId("download");
274
+ await this.db.insert(contentItems).values({
275
+ orgId,
276
+ id,
277
+ type: "download"
278
+ });
279
+ await this.db.insert(downloads).values({
280
+ orgId,
281
+ id,
282
+ title: input.title,
283
+ slug,
284
+ description: input.description ?? "",
285
+ category: input.category ?? ""
286
+ });
287
+ const created = await this.getDownload(orgId, id);
288
+ if (!created) throw new Error("failed to load created download");
289
+ return created;
290
+ }
291
+ async updateDownload(orgId, id, patch) {
292
+ const set = { updatedAt: /* @__PURE__ */ new Date() };
293
+ if (patch.title !== void 0) set.title = patch.title;
294
+ if (patch.description !== void 0) set.description = patch.description;
295
+ if (patch.category !== void 0) set.category = patch.category;
296
+ if (patch.status !== void 0) set.status = patch.status;
297
+ if (patch.thumbnailAssetId !== void 0) set.thumbnailAssetId = patch.thumbnailAssetId;
298
+ const [updated] = await this.db.update(downloads).set(set).where(and(eq(downloads.orgId, orgId), eq(downloads.id, id))).returning({ id: downloads.id });
299
+ if (!updated) return null;
300
+ return this.getDownload(orgId, id);
301
+ }
302
+ async deleteDownload(orgId, id) {
303
+ return (await this.db.delete(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, id), eq(contentItems.type, "download"))).returning({ id: contentItems.id })).length > 0;
304
+ }
305
+ async listDownloadAssets(orgId, downloadId) {
306
+ return this.db.select().from(downloadAssets).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId))).orderBy(asc(downloadAssets.seq));
307
+ }
308
+ /** Assert the download exists in the org; throw otherwise. */
309
+ async assertDownload(orgId, downloadId) {
310
+ const [row] = await this.db.select({ id: downloads.id }).from(downloads).where(and(eq(downloads.orgId, orgId), eq(downloads.id, downloadId))).limit(1);
311
+ if (!row) throw new NotFoundError("Download", downloadId);
312
+ }
313
+ async addDownloadAsset(orgId, downloadId, input) {
314
+ await this.assertDownload(orgId, downloadId);
315
+ const [maxRow] = await this.db.select({ maxSeq: sql`max(${downloadAssets.seq})` }).from(downloadAssets).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId)));
316
+ const nextSeq = (maxRow?.maxSeq ?? -1) + 1;
317
+ try {
318
+ await this.db.insert(downloadAssets).values({
319
+ orgId,
320
+ downloadId,
321
+ assetId: input.assetId,
322
+ seq: nextSeq,
323
+ displayName: input.displayName ?? null
324
+ });
325
+ } catch (err) {
326
+ if (isUniqueViolation(err)) throw new ConflictError("This asset is already linked to the download");
327
+ throw err;
328
+ }
329
+ return this.listDownloadAssets(orgId, downloadId);
330
+ }
331
+ async removeDownloadAsset(orgId, downloadId, assetId) {
332
+ await this.assertDownload(orgId, downloadId);
333
+ await this.db.delete(downloadAssets).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId), eq(downloadAssets.assetId, assetId)));
334
+ return this.listDownloadAssets(orgId, downloadId);
335
+ }
336
+ async renameDownloadAsset(orgId, downloadId, assetId, displayName) {
337
+ await this.assertDownload(orgId, downloadId);
338
+ await this.db.update(downloadAssets).set({ displayName }).where(and(eq(downloadAssets.orgId, orgId), eq(downloadAssets.downloadId, downloadId), eq(downloadAssets.assetId, assetId)));
339
+ return this.listDownloadAssets(orgId, downloadId);
340
+ }
341
+ async reorderDownloadAssets(orgId, downloadId, assetIds) {
342
+ await this.assertDownload(orgId, downloadId);
343
+ 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
+ return this.listDownloadAssets(orgId, downloadId);
345
+ }
346
+ };
347
+ //#endregion
348
+ export { DrizzleContentRepository };
349
+
350
+ //# sourceMappingURL=content.js.map
@@ -0,0 +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":"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"}
@@ -0,0 +1,25 @@
1
+ import { DbExecutor } from "../client.js";
2
+ import { CredentialStore, Logger } from "@headless-lms/core/shared/ports";
3
+ //#region src/repositories/credentials.d.ts
4
+ /** base64(iv ‖ auth tag ‖ ciphertext) */
5
+ declare function encryptSecret(key: Buffer, plaintext: string, aadValue: Buffer): string;
6
+ /** Throws if the payload was tampered with or the AAD/key doesn't match. */
7
+ declare function decryptSecret(key: Buffer, payload: string, aadValue: Buffer): string;
8
+ declare class DrizzleCredentialStore implements CredentialStore {
9
+ private readonly db;
10
+ /** base64-encoded 32-byte key (CREDENTIAL_STORE_KEY). Validated on first use, not at boot. */
11
+ private readonly keyBase64;
12
+ private readonly logger;
13
+ private key;
14
+ constructor(db: DbExecutor,
15
+ /** base64-encoded 32-byte key (CREDENTIAL_STORE_KEY). Validated on first use, not at boot. */
16
+ keyBase64: string, logger?: Logger);
17
+ private getKey;
18
+ store(orgId: string, secrets: Record<string, unknown>): Promise<string>;
19
+ reveal(orgId: string, ref: string): Promise<Record<string, unknown> | null>;
20
+ update(orgId: string, ref: string, secrets: Record<string, unknown>): Promise<void>;
21
+ destroy(orgId: string, ref: string): Promise<void>;
22
+ }
23
+ //#endregion
24
+ export { DrizzleCredentialStore, decryptSecret, encryptSecret };
25
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1,86 @@
1
+ import { credentials } from "../schema/credentials.js";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { genId } from "@headless-lms/core/shared/id";
4
+ import { noopLogger } from "@headless-lms/core/shared/logger";
5
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
6
+ //#region src/repositories/credentials.ts
7
+ const IV_BYTES = 12;
8
+ /** Version of the key currently used for new writes (stored per row for rotation). */
9
+ const KEY_VERSION = 1;
10
+ function aad(orgId, id) {
11
+ return Buffer.from(`${orgId}:${id}`, "utf8");
12
+ }
13
+ /** base64(iv ‖ auth tag ‖ ciphertext) */
14
+ function encryptSecret(key, plaintext, aadValue) {
15
+ const iv = randomBytes(IV_BYTES);
16
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
17
+ cipher.setAAD(aadValue);
18
+ const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
19
+ return Buffer.concat([
20
+ iv,
21
+ cipher.getAuthTag(),
22
+ data
23
+ ]).toString("base64");
24
+ }
25
+ /** Throws if the payload was tampered with or the AAD/key doesn't match. */
26
+ function decryptSecret(key, payload, aadValue) {
27
+ const buf = Buffer.from(payload, "base64");
28
+ const iv = buf.subarray(0, IV_BYTES);
29
+ const tag = buf.subarray(IV_BYTES, 28);
30
+ const data = buf.subarray(28);
31
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
32
+ decipher.setAAD(aadValue);
33
+ decipher.setAuthTag(tag);
34
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
35
+ }
36
+ var DrizzleCredentialStore = class {
37
+ db;
38
+ keyBase64;
39
+ logger;
40
+ key;
41
+ constructor(db, keyBase64, logger = noopLogger) {
42
+ this.db = db;
43
+ this.keyBase64 = keyBase64;
44
+ this.logger = logger;
45
+ }
46
+ getKey() {
47
+ if (!this.key) {
48
+ const key = Buffer.from(this.keyBase64, "base64");
49
+ if (key.length !== 32) throw new Error("CREDENTIAL_STORE_KEY must be a base64-encoded 32-byte key (generate with: openssl rand -base64 32)");
50
+ this.key = key;
51
+ }
52
+ return this.key;
53
+ }
54
+ async store(orgId, secrets) {
55
+ const id = genId("credential");
56
+ const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, id));
57
+ await this.db.insert(credentials).values({
58
+ orgId,
59
+ id,
60
+ ciphertext,
61
+ keyVersion: KEY_VERSION
62
+ });
63
+ return id;
64
+ }
65
+ async reveal(orgId, ref) {
66
+ const [row] = await this.db.select().from(credentials).where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref))).limit(1);
67
+ if (!row) return null;
68
+ const plaintext = decryptSecret(this.getKey(), row.ciphertext, aad(orgId, ref));
69
+ return JSON.parse(plaintext);
70
+ }
71
+ async update(orgId, ref, secrets) {
72
+ const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, ref));
73
+ await this.db.update(credentials).set({
74
+ ciphertext,
75
+ keyVersion: KEY_VERSION,
76
+ updatedAt: /* @__PURE__ */ new Date()
77
+ }).where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));
78
+ }
79
+ async destroy(orgId, ref) {
80
+ await this.db.delete(credentials).where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));
81
+ }
82
+ };
83
+ //#endregion
84
+ export { DrizzleCredentialStore, decryptSecret, encryptSecret };
85
+
86
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","names":[],"sources":["../../src/repositories/credentials.ts"],"sourcesContent":["// credentials — Drizzle-backed secure credential store (implements the shared\n// core port). Secrets are JSON documents: serialized here, encrypted with\n// AES-256-GCM via node:crypto (single 32-byte key from the environment), and\n// parsed back to objects on reveal — callers never (de)serialize. The AAD\n// binds each ciphertext to its (org, id) row, so moving a ciphertext to\n// another row or org makes it undecryptable. Plaintext exists only inside\n// store/reveal — it is never logged, listed, or returned elsewhere.\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { and, eq } from 'drizzle-orm';\nimport type { DbExecutor } from '../client.js';\nimport type { CredentialStore, Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\nimport { genId } from '@headless-lms/core/shared/id';\nimport { credentials } from '../schema/credentials.js';\n\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\n/** Version of the key currently used for new writes (stored per row for rotation). */\nconst KEY_VERSION = 1;\n\nfunction aad(orgId: string, id: string): Buffer {\n return Buffer.from(`${orgId}:${id}`, 'utf8');\n}\n\n/** base64(iv ‖ auth tag ‖ ciphertext) */\nexport function encryptSecret(key: Buffer, plaintext: string, aadValue: Buffer): string {\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n cipher.setAAD(aadValue);\n const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n return Buffer.concat([iv, cipher.getAuthTag(), data]).toString('base64');\n}\n\n/** Throws if the payload was tampered with or the AAD/key doesn't match. */\nexport function decryptSecret(key: Buffer, payload: string, aadValue: Buffer): string {\n const buf = Buffer.from(payload, 'base64');\n const iv = buf.subarray(0, IV_BYTES);\n const tag = buf.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);\n const data = buf.subarray(IV_BYTES + TAG_BYTES);\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAAD(aadValue);\n decipher.setAuthTag(tag);\n return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');\n}\n\nexport class DrizzleCredentialStore implements CredentialStore {\n private key: Buffer | undefined;\n\n constructor(\n private readonly db: DbExecutor,\n /** base64-encoded 32-byte key (CREDENTIAL_STORE_KEY). Validated on first use, not at boot. */\n private readonly keyBase64: string,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n private getKey(): Buffer {\n if (!this.key) {\n const key = Buffer.from(this.keyBase64, 'base64');\n if (key.length !== 32) {\n throw new Error(\n 'CREDENTIAL_STORE_KEY must be a base64-encoded 32-byte key (generate with: openssl rand -base64 32)',\n );\n }\n this.key = key;\n }\n return this.key;\n }\n\n async store(orgId: string, secrets: Record<string, unknown>): Promise<string> {\n const id = genId('credential');\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, id));\n await this.db.insert(credentials).values({ orgId, id, ciphertext, keyVersion: KEY_VERSION });\n return id;\n }\n\n async reveal(orgId: string, ref: string): Promise<Record<string, unknown> | null> {\n const [row] = await this.db\n .select()\n .from(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)))\n .limit(1);\n if (!row) {\n return null;\n }\n const plaintext = decryptSecret(this.getKey(), row.ciphertext, aad(orgId, ref));\n return JSON.parse(plaintext) as Record<string, unknown>;\n }\n\n async update(orgId: string, ref: string, secrets: Record<string, unknown>): Promise<void> {\n const ciphertext = encryptSecret(this.getKey(), JSON.stringify(secrets), aad(orgId, ref));\n await this.db\n .update(credentials)\n .set({ ciphertext, keyVersion: KEY_VERSION, updatedAt: new Date() })\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n\n async destroy(orgId: string, ref: string): Promise<void> {\n await this.db\n .delete(credentials)\n .where(and(eq(credentials.orgId, orgId), eq(credentials.id, ref)));\n }\n}\n"],"mappings":";;;;;;AAeA,MAAM,WAAW;;AAGjB,MAAM,cAAc;AAEpB,SAAS,IAAI,OAAe,IAAoB;CAC9C,OAAO,OAAO,KAAK,GAAG,MAAM,GAAG,MAAM,MAAM;AAC7C;;AAGA,SAAgB,cAAc,KAAa,WAAmB,UAA0B;CACtF,MAAM,KAAK,YAAY,QAAQ;CAC/B,MAAM,SAAS,eAAe,eAAe,KAAK,EAAE;CACpD,OAAO,OAAO,QAAQ;CACtB,MAAM,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;CAC7E,OAAO,OAAO,OAAO;EAAC;EAAI,OAAO,WAAW;EAAG;CAAI,CAAC,CAAC,CAAC,SAAS,QAAQ;AACzE;;AAGA,SAAgB,cAAc,KAAa,SAAiB,UAA0B;CACpF,MAAM,MAAM,OAAO,KAAK,SAAS,QAAQ;CACzC,MAAM,KAAK,IAAI,SAAS,GAAG,QAAQ;CACnC,MAAM,MAAM,IAAI,SAAS,UAAU,EAAoB;CACvD,MAAM,OAAO,IAAI,SAAS,EAAoB;CAC9C,MAAM,WAAW,iBAAiB,eAAe,KAAK,EAAE;CACxD,SAAS,OAAO,QAAQ;CACxB,SAAS,WAAW,GAAG;CACvB,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM;AACjF;AAEA,IAAa,yBAAb,MAA+D;CAI1C;CAEA;CACA;CANnB;CAEA,YACE,IAEA,WACA,SAAkC,YAClC;EAJiB,KAAA,KAAA;EAEA,KAAA,YAAA;EACA,KAAA,SAAA;CAChB;CAEH,SAAyB;EACvB,IAAI,CAAC,KAAK,KAAK;GACb,MAAM,MAAM,OAAO,KAAK,KAAK,WAAW,QAAQ;GAChD,IAAI,IAAI,WAAW,IACjB,MAAM,IAAI,MACR,oGACF;GAEF,KAAK,MAAM;EACb;EACA,OAAO,KAAK;CACd;CAEA,MAAM,MAAM,OAAe,SAAmD;EAC5E,MAAM,KAAK,MAAM,YAAY;EAC7B,MAAM,aAAa,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;EACvF,MAAM,KAAK,GAAG,OAAO,WAAW,CAAC,CAAC,OAAO;GAAE;GAAO;GAAI;GAAY,YAAY;EAAY,CAAC;EAC3F,OAAO;CACT;CAEA,MAAM,OAAO,OAAe,KAAsD;EAChF,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,CAAC,CACR,KAAK,WAAW,CAAC,CACjB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC,CAAC,CACjE,MAAM,CAAC;EACV,IAAI,CAAC,KACH,OAAO;EAET,MAAM,YAAY,cAAc,KAAK,OAAO,GAAG,IAAI,YAAY,IAAI,OAAO,GAAG,CAAC;EAC9E,OAAO,KAAK,MAAM,SAAS;CAC7B;CAEA,MAAM,OAAO,OAAe,KAAa,SAAiD;EACxF,MAAM,aAAa,cAAc,KAAK,OAAO,GAAG,KAAK,UAAU,OAAO,GAAG,IAAI,OAAO,GAAG,CAAC;EACxF,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,IAAI;GAAE;GAAY,YAAY;GAAa,2BAAW,IAAI,KAAK;EAAE,CAAC,CAAC,CACnE,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC;CACrE;CAEA,MAAM,QAAQ,OAAe,KAA4B;EACvD,MAAM,KAAK,GACR,OAAO,WAAW,CAAC,CACnB,MAAM,IAAI,GAAG,YAAY,OAAO,KAAK,GAAG,GAAG,YAAY,IAAI,GAAG,CAAC,CAAC;CACrE;AACF"}
@@ -0,0 +1,14 @@
1
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { Logger } from "@headless-lms/core/shared/ports";
3
+ import { DashboardReportRepository, EnrollmentPoint, OverviewStats } from "@headless-lms/core/reporting/dashboard";
4
+ //#region src/repositories/dashboard.d.ts
5
+ declare class DrizzleDashboardRepository implements DashboardReportRepository {
6
+ private readonly db;
7
+ private readonly logger;
8
+ constructor(db: NodePgDatabase, logger?: Logger);
9
+ overview(orgId: string): Promise<OverviewStats>;
10
+ enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]>;
11
+ }
12
+ //#endregion
13
+ export { DrizzleDashboardRepository };
14
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { courses } from "../schema/content.js";
2
+ import { entitlements } from "../schema/entitlements.js";
3
+ import "../schema/index.js";
4
+ import { and, eq, gte, isNull, or, sql } from "drizzle-orm";
5
+ import { noopLogger } from "@headless-lms/core/shared/logger";
6
+ //#region src/repositories/dashboard.ts
7
+ var DrizzleDashboardRepository = class {
8
+ db;
9
+ logger;
10
+ constructor(db, logger = noopLogger) {
11
+ this.db = db;
12
+ this.logger = logger;
13
+ }
14
+ async overview(orgId) {
15
+ const [courseCounts] = await this.db.select({
16
+ published: sql`count(*) filter (where ${courses.status} = 'published')`,
17
+ draft: sql`count(*) filter (where ${courses.status} = 'draft')`
18
+ }).from(courses).where(eq(courses.orgId, orgId));
19
+ const effectiveActive = and(eq(entitlements.orgId, orgId), eq(entitlements.status, "active"), or(isNull(entitlements.expiresAt), gte(entitlements.expiresAt, sql`now()`)));
20
+ const [entitlementCounts] = await this.db.select({
21
+ activeStudents: sql`count(distinct ${entitlements.orgUserId})`,
22
+ expiringSoon: sql`count(*) filter (where ${entitlements.expiresAt} is not null and ${entitlements.expiresAt} < now() + interval '14 days')`
23
+ }).from(entitlements).where(effectiveActive);
24
+ return {
25
+ publishedCourses: Number(courseCounts?.published ?? 0),
26
+ draftCourses: Number(courseCounts?.draft ?? 0),
27
+ activeStudents: Number(entitlementCounts?.activeStudents ?? 0),
28
+ expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0)
29
+ };
30
+ }
31
+ async enrollments(orgId, days) {
32
+ return (await this.db.execute(sql`
33
+ select
34
+ to_char(d.day, 'YYYY-MM-DD') as date,
35
+ count(${entitlements.id})::int as count
36
+ from generate_series(
37
+ (now() at time zone 'utc')::date - (${days} - 1),
38
+ (now() at time zone 'utc')::date,
39
+ interval '1 day'
40
+ ) as d(day)
41
+ left join ${entitlements}
42
+ on ${entitlements.orgId} = ${orgId}
43
+ and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date
44
+ group by d.day
45
+ order by d.day
46
+ `)).rows.map((r) => ({
47
+ date: r.date,
48
+ count: Number(r.count)
49
+ }));
50
+ }
51
+ };
52
+ //#endregion
53
+ export { DrizzleDashboardRepository };
54
+
55
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","names":[],"sources":["../../src/repositories/dashboard.ts"],"sourcesContent":["// dashboard — Drizzle repository (implements the core outbound port).\n// Back-office overview counts, every figure scoped to the active org. An\n// entitlement is \"effective-active\" when status='active' and it has not expired\n// (expires_at null or in the future) — expiry is derived at read time.\nimport { and, eq, gte, isNull, or, sql } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type {\n DashboardReportRepository,\n EnrollmentPoint,\n OverviewStats,\n} from '@headless-lms/core/reporting/dashboard';\nimport { courses, entitlements } from '../schema/index.js';\nimport type { Logger } from '@headless-lms/core/shared/ports';\nimport { noopLogger } from '@headless-lms/core/shared/logger';\n\nexport class DrizzleDashboardRepository implements DashboardReportRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async overview(orgId: string): Promise<OverviewStats> {\n const [courseCounts] = await this.db\n .select({\n published: sql<number>`count(*) filter (where ${courses.status} = 'published')`,\n draft: sql<number>`count(*) filter (where ${courses.status} = 'draft')`,\n })\n .from(courses)\n .where(eq(courses.orgId, orgId));\n\n const effectiveActive = and(\n eq(entitlements.orgId, orgId),\n eq(entitlements.status, 'active'),\n or(isNull(entitlements.expiresAt), gte(entitlements.expiresAt, sql`now()`)),\n );\n\n const [entitlementCounts] = await this.db\n .select({\n activeStudents: sql<number>`count(distinct ${entitlements.orgUserId})`,\n expiringSoon: sql<number>`count(*) filter (where ${entitlements.expiresAt} is not null and ${entitlements.expiresAt} < now() + interval '14 days')`,\n })\n .from(entitlements)\n .where(effectiveActive);\n\n return {\n publishedCourses: Number(courseCounts?.published ?? 0),\n draftCourses: Number(courseCounts?.draft ?? 0),\n activeStudents: Number(entitlementCounts?.activeStudents ?? 0),\n expiringSoon: Number(entitlementCounts?.expiringSoon ?? 0),\n };\n }\n\n // Grants per day over the trailing window, zero-filled so the series has one\n // point per calendar day (UTC) even when nothing was granted.\n async enrollments(orgId: string, days: number): Promise<EnrollmentPoint[]> {\n const result = await this.db.execute(sql`\n select\n to_char(d.day, 'YYYY-MM-DD') as date,\n count(${entitlements.id})::int as count\n from generate_series(\n (now() at time zone 'utc')::date - (${days} - 1),\n (now() at time zone 'utc')::date,\n interval '1 day'\n ) as d(day)\n left join ${entitlements}\n on ${entitlements.orgId} = ${orgId}\n and (${entitlements.grantedAt} at time zone 'utc')::date = d.day::date\n group by d.day\n order by d.day\n `);\n const rows = result.rows as { date: string; count: number }[];\n return rows.map((r) => ({ date: r.date, count: Number(r.count) }));\n }\n}\n"],"mappings":";;;;;;AAeA,IAAa,6BAAb,MAA6E;CAExD;CACA;CAFnB,YACE,IACA,SAAkC,YAClC;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,SAAS,OAAuC;EACpD,MAAM,CAAC,gBAAgB,MAAM,KAAK,GAC/B,OAAO;GACN,WAAW,GAAW,0BAA0B,QAAQ,OAAO;GAC/D,OAAO,GAAW,0BAA0B,QAAQ,OAAO;EAC7D,CAAC,CAAC,CACD,KAAK,OAAO,CAAC,CACb,MAAM,GAAG,QAAQ,OAAO,KAAK,CAAC;EAEjC,MAAM,kBAAkB,IACtB,GAAG,aAAa,OAAO,KAAK,GAC5B,GAAG,aAAa,QAAQ,QAAQ,GAChC,GAAG,OAAO,aAAa,SAAS,GAAG,IAAI,aAAa,WAAW,GAAG,OAAO,CAAC,CAC5E;EAEA,MAAM,CAAC,qBAAqB,MAAM,KAAK,GACpC,OAAO;GACN,gBAAgB,GAAW,kBAAkB,aAAa,UAAU;GACpE,cAAc,GAAW,0BAA0B,aAAa,UAAU,mBAAmB,aAAa,UAAU;EACtH,CAAC,CAAC,CACD,KAAK,YAAY,CAAC,CAClB,MAAM,eAAe;EAExB,OAAO;GACL,kBAAkB,OAAO,cAAc,aAAa,CAAC;GACrD,cAAc,OAAO,cAAc,SAAS,CAAC;GAC7C,gBAAgB,OAAO,mBAAmB,kBAAkB,CAAC;GAC7D,cAAc,OAAO,mBAAmB,gBAAgB,CAAC;EAC3D;CACF;CAIA,MAAM,YAAY,OAAe,MAA0C;EAiBzE,QADa,MAfQ,KAAK,GAAG,QAAQ,GAAG;;;gBAG5B,aAAa,GAAG;;8CAEc,KAAK;;;;kBAIjC,aAAa;aAClB,aAAa,MAAM,KAAK,MAAM;eAC5B,aAAa,UAAU;;;KAGjC,EAAA,CACmB,KACR,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,OAAO,OAAO,EAAE,KAAK;EAAE,EAAE;CACnE;AACF"}