@things-factory/learning 0.0.0-stage → 10.1.260

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 (149) hide show
  1. package/dist-client/bootstrap.d.ts +2 -0
  2. package/dist-client/bootstrap.js +4 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/components/learning-assign-form.d.ts +24 -0
  5. package/dist-client/components/learning-assign-form.js +77 -0
  6. package/dist-client/components/learning-assign-form.js.map +1 -0
  7. package/dist-client/components/learning-course-form.d.ts +29 -0
  8. package/dist-client/components/learning-course-form.js +104 -0
  9. package/dist-client/components/learning-course-form.js.map +1 -0
  10. package/dist-client/components/learning-question-form.d.ts +33 -0
  11. package/dist-client/components/learning-question-form.js +142 -0
  12. package/dist-client/components/learning-question-form.js.map +1 -0
  13. package/dist-client/components/learning-todo.d.ts +12 -0
  14. package/dist-client/components/learning-todo.js +43 -0
  15. package/dist-client/components/learning-todo.js.map +1 -0
  16. package/dist-client/components/learning-unit-form.d.ts +41 -0
  17. package/dist-client/components/learning-unit-form.js +143 -0
  18. package/dist-client/components/learning-unit-form.js.map +1 -0
  19. package/dist-client/index.d.ts +2 -0
  20. package/dist-client/index.js +3 -0
  21. package/dist-client/index.js.map +1 -0
  22. package/dist-client/pages/learning-course-editor.d.ts +62 -0
  23. package/dist-client/pages/learning-course-editor.js +463 -0
  24. package/dist-client/pages/learning-course-editor.js.map +1 -0
  25. package/dist-client/pages/learning-course-list.d.ts +37 -0
  26. package/dist-client/pages/learning-course-list.js +88 -0
  27. package/dist-client/pages/learning-course-list.js.map +1 -0
  28. package/dist-client/pages/learning-enrollment-status.d.ts +53 -0
  29. package/dist-client/pages/learning-enrollment-status.js +228 -0
  30. package/dist-client/pages/learning-enrollment-status.js.map +1 -0
  31. package/dist-client/pages/learning-view.d.ts +52 -0
  32. package/dist-client/pages/learning-view.js +371 -0
  33. package/dist-client/pages/learning-view.js.map +1 -0
  34. package/dist-client/pages/my-learning.d.ts +22 -0
  35. package/dist-client/pages/my-learning.js +110 -0
  36. package/dist-client/pages/my-learning.js.map +1 -0
  37. package/dist-client/route.d.ts +1 -0
  38. package/dist-client/route.js +21 -0
  39. package/dist-client/route.js.map +1 -0
  40. package/dist-client/tsconfig.tsbuildinfo +1 -0
  41. package/dist-client/utils/learning-api.d.ts +16 -0
  42. package/dist-client/utils/learning-api.js +36 -0
  43. package/dist-client/utils/learning-api.js.map +1 -0
  44. package/dist-client/utils/learning-words.d.ts +20 -0
  45. package/dist-client/utils/learning-words.js +22 -0
  46. package/dist-client/utils/learning-words.js.map +1 -0
  47. package/dist-server/activities.d.ts +38 -0
  48. package/dist-server/activities.js +42 -0
  49. package/dist-server/activities.js.map +1 -0
  50. package/dist-server/ai-reads.d.ts +9 -0
  51. package/dist-server/ai-reads.js +70 -0
  52. package/dist-server/ai-reads.js.map +1 -0
  53. package/dist-server/attachment-reader.d.ts +1 -0
  54. package/dist-server/attachment-reader.js +23 -0
  55. package/dist-server/attachment-reader.js.map +1 -0
  56. package/dist-server/index.d.ts +4 -0
  57. package/dist-server/index.js +33 -0
  58. package/dist-server/index.js.map +1 -0
  59. package/dist-server/learning-subjects.d.ts +10 -0
  60. package/dist-server/learning-subjects.js +31 -0
  61. package/dist-server/learning-subjects.js.map +1 -0
  62. package/dist-server/role-tasks.d.ts +2 -0
  63. package/dist-server/role-tasks.js +17 -0
  64. package/dist-server/role-tasks.js.map +1 -0
  65. package/dist-server/service/course/course-mutation.d.ts +19 -0
  66. package/dist-server/service/course/course-mutation.js +176 -0
  67. package/dist-server/service/course/course-mutation.js.map +1 -0
  68. package/dist-server/service/course/course-query.d.ts +26 -0
  69. package/dist-server/service/course/course-query.js +172 -0
  70. package/dist-server/service/course/course-query.js.map +1 -0
  71. package/dist-server/service/course/course-question.d.ts +41 -0
  72. package/dist-server/service/course/course-question.js +116 -0
  73. package/dist-server/service/course/course-question.js.map +1 -0
  74. package/dist-server/service/course/course-rules.d.ts +49 -0
  75. package/dist-server/service/course/course-rules.js +89 -0
  76. package/dist-server/service/course/course-rules.js.map +1 -0
  77. package/dist-server/service/course/course-service.d.ts +71 -0
  78. package/dist-server/service/course/course-service.js +402 -0
  79. package/dist-server/service/course/course-service.js.map +1 -0
  80. package/dist-server/service/course/course-type.d.ts +55 -0
  81. package/dist-server/service/course/course-type.js +198 -0
  82. package/dist-server/service/course/course-type.js.map +1 -0
  83. package/dist-server/service/course/course-unit.d.ts +36 -0
  84. package/dist-server/service/course/course-unit.js +123 -0
  85. package/dist-server/service/course/course-unit.js.map +1 -0
  86. package/dist-server/service/course/course-version.d.ts +45 -0
  87. package/dist-server/service/course/course-version.js +151 -0
  88. package/dist-server/service/course/course-version.js.map +1 -0
  89. package/dist-server/service/course/course.d.ts +23 -0
  90. package/dist-server/service/course/course.js +92 -0
  91. package/dist-server/service/course/course.js.map +1 -0
  92. package/dist-server/service/course/index.d.ts +14 -0
  93. package/dist-server/service/course/index.js +24 -0
  94. package/dist-server/service/course/index.js.map +1 -0
  95. package/dist-server/service/enrollment/enrollment-mutation.d.ts +11 -0
  96. package/dist-server/service/enrollment/enrollment-mutation.js +124 -0
  97. package/dist-server/service/enrollment/enrollment-mutation.js.map +1 -0
  98. package/dist-server/service/enrollment/enrollment-query.d.ts +23 -0
  99. package/dist-server/service/enrollment/enrollment-query.js +245 -0
  100. package/dist-server/service/enrollment/enrollment-query.js.map +1 -0
  101. package/dist-server/service/enrollment/enrollment-remind.d.ts +1 -0
  102. package/dist-server/service/enrollment/enrollment-remind.js +45 -0
  103. package/dist-server/service/enrollment/enrollment-remind.js.map +1 -0
  104. package/dist-server/service/enrollment/enrollment-service.d.ts +110 -0
  105. package/dist-server/service/enrollment/enrollment-service.js +417 -0
  106. package/dist-server/service/enrollment/enrollment-service.js.map +1 -0
  107. package/dist-server/service/enrollment/enrollment-status.d.ts +40 -0
  108. package/dist-server/service/enrollment/enrollment-status.js +135 -0
  109. package/dist-server/service/enrollment/enrollment-status.js.map +1 -0
  110. package/dist-server/service/enrollment/enrollment-type.d.ts +95 -0
  111. package/dist-server/service/enrollment/enrollment-type.js +341 -0
  112. package/dist-server/service/enrollment/enrollment-type.js.map +1 -0
  113. package/dist-server/service/enrollment/enrollment.d.ts +52 -0
  114. package/dist-server/service/enrollment/enrollment.js +186 -0
  115. package/dist-server/service/enrollment/enrollment.js.map +1 -0
  116. package/dist-server/service/enrollment/index.d.ts +15 -0
  117. package/dist-server/service/enrollment/index.js +26 -0
  118. package/dist-server/service/enrollment/index.js.map +1 -0
  119. package/dist-server/service/enrollment/learning-ai.d.ts +38 -0
  120. package/dist-server/service/enrollment/learning-ai.js +96 -0
  121. package/dist-server/service/enrollment/learning-ai.js.map +1 -0
  122. package/dist-server/service/enrollment/learning-documents.d.ts +7 -0
  123. package/dist-server/service/enrollment/learning-documents.js +160 -0
  124. package/dist-server/service/enrollment/learning-documents.js.map +1 -0
  125. package/dist-server/service/enrollment/learning-outcome.d.ts +10 -0
  126. package/dist-server/service/enrollment/learning-outcome.js +16 -0
  127. package/dist-server/service/enrollment/learning-outcome.js.map +1 -0
  128. package/dist-server/service/enrollment/progress-rules.d.ts +94 -0
  129. package/dist-server/service/enrollment/progress-rules.js +97 -0
  130. package/dist-server/service/enrollment/progress-rules.js.map +1 -0
  131. package/dist-server/service/enrollment/quiz-attempt.d.ts +34 -0
  132. package/dist-server/service/enrollment/quiz-attempt.js +105 -0
  133. package/dist-server/service/enrollment/quiz-attempt.js.map +1 -0
  134. package/dist-server/service/enrollment/unit-progress.d.ts +30 -0
  135. package/dist-server/service/enrollment/unit-progress.js +114 -0
  136. package/dist-server/service/enrollment/unit-progress.js.map +1 -0
  137. package/dist-server/service/index.d.ts +14 -0
  138. package/dist-server/service/index.js +43 -0
  139. package/dist-server/service/index.js.map +1 -0
  140. package/dist-server/tsconfig.tsbuildinfo +1 -0
  141. package/package.json +56 -4
  142. package/things-factory.config.js +14 -0
  143. package/translations/en.json +255 -0
  144. package/translations/es.json +255 -0
  145. package/translations/ja.json +255 -0
  146. package/translations/ko.json +255 -0
  147. package/translations/ms.json +255 -0
  148. package/translations/zh.json +255 -0
  149. package/README.md +0 -3
@@ -0,0 +1,402 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COURSE_UNIT_REF_TYPE = void 0;
4
+ exports.learningManagerOf = learningManagerOf;
5
+ exports.holds = holds;
6
+ exports.mayReadVersion = mayReadVersion;
7
+ exports.courseOf = courseOf;
8
+ exports.createCourse = createCourse;
9
+ exports.updateCourse = updateCourse;
10
+ exports.versionOf = versionOf;
11
+ exports.publishedVersionOf = publishedVersionOf;
12
+ exports.unitsOf = unitsOf;
13
+ exports.questionsOf = questionsOf;
14
+ exports.createCourseVersion = createCourseVersion;
15
+ exports.updateCourseVersion = updateCourseVersion;
16
+ exports.saveCourseUnits = saveCourseUnits;
17
+ exports.attachCourseUnitFile = attachCourseUnitFile;
18
+ exports.saveCourseQuestions = saveCourseQuestions;
19
+ exports.addDraftedQuestions = addDraftedQuestions;
20
+ exports.publishCourseVersion = publishCourseVersion;
21
+ exports.closeCourseVersion = closeCourseVersion;
22
+ exports.deleteCourseVersion = deleteCourseVersion;
23
+ exports.unitFilesOf = unitFilesOf;
24
+ /*
25
+ * What the doors of courses do with the database (ADR-0194 decisions 2-4): a course, its versions written as drafts,
26
+ * their units and questions, publishing one (the one before closes) and copying one into the next draft. Nothing here
27
+ * changes a published version.
28
+ */
29
+ const crypto_1 = require("crypto");
30
+ const stream_1 = require("stream");
31
+ const typeorm_1 = require("typeorm");
32
+ const attachment_base_1 = require("@things-factory/attachment-base");
33
+ const auth_base_1 = require("@things-factory/auth-base");
34
+ const shell_1 = require("@things-factory/shell");
35
+ const course_1 = require("./course");
36
+ const course_question_1 = require("./course-question");
37
+ const course_rules_1 = require("./course-rules");
38
+ const course_unit_1 = require("./course-unit");
39
+ const course_version_1 = require("./course-version");
40
+ /** A unit's file hangs on it under this `refType` (attachment-base). */
41
+ exports.COURSE_UNIT_REF_TYPE = 'CourseUnit';
42
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
43
+ /** The request's transaction, or the data source's manager for a read that runs without one. */
44
+ function learningManagerOf(context) {
45
+ return context?.state?.tx ?? (0, shell_1.getRepository)(course_1.Course).manager;
46
+ }
47
+ /** Whether the request holds `category` of this module (`course`, `learning`) - asked once per request. */
48
+ async function holds(context, category, privilege = 'mutation') {
49
+ const state = context?.state ?? {};
50
+ const { domain, user, unsafeIP, prohibitedPrivileges } = state;
51
+ if (!domain || !user)
52
+ return false;
53
+ const key = `__learning:${category}:${privilege}`;
54
+ state[key] ??= (0, auth_base_1.checkPermission)({ category, privilege, owner: true }, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean);
55
+ return await state[key];
56
+ }
57
+ /**
58
+ * Whether the request may read what a version says - its units and their files: a manager (`course` query) any version;
59
+ * anyone else only the version an enrolment of theirs shows - its own once started, the published one before. Asked on
60
+ * every path to a version, not only the top-level reads: a learner reached a draft through
61
+ * `myEnrollment → course → versions → units` (2026-11-08).
62
+ */
63
+ async function mayReadVersion(context, version) {
64
+ if (!version)
65
+ return false;
66
+ if (await holds(context, 'course', 'query'))
67
+ return true;
68
+ const { domain, user } = context?.state ?? {};
69
+ if (!domain || !user)
70
+ return false;
71
+ const state = context.state;
72
+ state.__learningShown ??= learningManagerOf(context)
73
+ .getRepository('Enrollment')
74
+ .find({ where: { domain: { id: domain.id }, user: { id: user.id }, status: (0, typeorm_1.In)(['ASSIGNED', 'IN_PROGRESS', 'COMPLETED']) }, relations: ['course', 'version'] });
75
+ const mine = await state.__learningShown;
76
+ return mine.some(one => one.course?.id === version.courseId && (one.version ? one.version.id === version.id : version.status === course_version_1.CourseVersionStatus.PUBLISHED));
77
+ }
78
+ const notFound = () => new shell_1.Refusal('learning-not-found', {}, 'the record was not found. reload and try again.');
79
+ function refuse(code) {
80
+ if (code)
81
+ throw new shell_1.Refusal(code, {}, `refused (${code})`);
82
+ }
83
+ /* ---------------------------------------------------------------- courses */
84
+ async function courseOf(tx, domain, id) {
85
+ const row = id ? await tx.getRepository(course_1.Course).findOne({ where: { id, domain: { id: domain.id } } }) : null;
86
+ if (!row)
87
+ throw notFound();
88
+ return row;
89
+ }
90
+ async function managerOf(tx, domain, managerId, fallback) {
91
+ if (!managerId)
92
+ return fallback;
93
+ const user = await tx.getRepository(auth_base_1.User).findOne({ where: { id: managerId }, relations: ['domains'] });
94
+ if (!user || !(user.domains ?? []).some(one => one.id === domain.id))
95
+ throw new shell_1.Refusal('learning-manager-unknown', {}, 'the manager must be a user of this domain');
96
+ return user;
97
+ }
98
+ async function createCourse(input, context) {
99
+ const { domain, user, tx } = context.state;
100
+ const name = text(input.name);
101
+ if (!name)
102
+ throw new shell_1.Refusal('learning-course-name-required', {}, 'give the course a name');
103
+ if (await tx.getRepository(course_1.Course).findOne({ where: { domain: { id: domain.id }, name } }))
104
+ throw new shell_1.Refusal('learning-course-name-taken', { name }, `a course named ${name} exists`);
105
+ return await tx.getRepository(course_1.Course).save({
106
+ domain,
107
+ name,
108
+ description: text(input.description),
109
+ category: text(input.category),
110
+ manager: await managerOf(tx, domain, input.managerId, user),
111
+ creator: user,
112
+ updater: user
113
+ });
114
+ }
115
+ async function updateCourse(id, patch, context) {
116
+ const { domain, user, tx } = context.state;
117
+ const row = await courseOf(tx, domain, id);
118
+ if (patch.name !== undefined) {
119
+ const name = text(patch.name);
120
+ if (!name)
121
+ throw new shell_1.Refusal('learning-course-name-required', {}, 'give the course a name');
122
+ const taken = await tx.getRepository(course_1.Course).findOne({ where: { domain: { id: domain.id }, name, id: (0, typeorm_1.Not)(id) } });
123
+ if (taken)
124
+ throw new shell_1.Refusal('learning-course-name-taken', { name }, `a course named ${name} exists`);
125
+ row.name = name;
126
+ }
127
+ if (patch.description !== undefined)
128
+ row.description = text(patch.description);
129
+ if (patch.category !== undefined)
130
+ row.category = text(patch.category);
131
+ if (patch.managerId !== undefined)
132
+ row.manager = await managerOf(tx, domain, patch.managerId, user);
133
+ row.updater = user;
134
+ return await tx.getRepository(course_1.Course).save(row);
135
+ }
136
+ /* ---------------------------------------------------------------- versions */
137
+ async function versionOf(tx, domain, id) {
138
+ const row = id ? await tx.getRepository(course_version_1.CourseVersion).findOne({ where: { id, domain: { id: domain.id } } }) : null;
139
+ if (!row)
140
+ throw notFound();
141
+ return row;
142
+ }
143
+ /** The version learners take now - the one published, or none. */
144
+ async function publishedVersionOf(tx, domain, courseId) {
145
+ return await tx.getRepository(course_version_1.CourseVersion).findOne({ where: { domain: { id: domain.id }, course: { id: courseId }, status: course_version_1.CourseVersionStatus.PUBLISHED } });
146
+ }
147
+ async function unitsOf(tx, versionId) {
148
+ return await tx.getRepository(course_unit_1.CourseUnit).find({ where: { version: { id: versionId } }, order: { seq: 'ASC' } });
149
+ }
150
+ async function questionsOf(tx, versionId) {
151
+ return await tx.getRepository(course_question_1.CourseQuestion).find({ where: { version: { id: versionId } }, order: { seq: 'ASC' } });
152
+ }
153
+ /** A draft version to write in, refused while the version is published or closed (decision 2). */
154
+ async function draftOf(tx, domain, id) {
155
+ const version = await versionOf(tx, domain, id);
156
+ refuse((0, course_rules_1.versionEditRefusal)(version));
157
+ return version;
158
+ }
159
+ /** The file of a unit, copied into a new file of another unit - each version keeps its own. */
160
+ async function copyFile(from, toUnitId, context) {
161
+ const bytes = Buffer.from(await attachment_base_1.STORAGE.readFile(from.path));
162
+ const file = Promise.resolve({ filename: from.name, mimetype: from.mimetype, encoding: from.encoding, createReadStream: () => stream_1.Readable.from([bytes]) });
163
+ await (0, attachment_base_1.createAttachment)(null, { attachment: { file, refType: exports.COURSE_UNIT_REF_TYPE, refBy: toUnitId } }, context);
164
+ }
165
+ /**
166
+ * A new draft of a course - the next number, with what `fromId` says copied (the latest version when not given): its
167
+ * settings, units with their files, and questions. One draft at a time.
168
+ */
169
+ async function createCourseVersion(courseId, fromId, context) {
170
+ const { domain, user, tx } = context.state;
171
+ const course = await courseOf(tx, domain, courseId);
172
+ const repository = tx.getRepository(course_version_1.CourseVersion);
173
+ const versions = await repository.find({ where: { domain: { id: domain.id }, course: { id: course.id } }, order: { number: 'DESC' } });
174
+ if (versions.some(one => one.status === course_version_1.CourseVersionStatus.DRAFT))
175
+ throw new shell_1.Refusal('learning-draft-exists', {}, 'the course has a draft version - write in it');
176
+ const from = fromId ? versions.find(one => one.id === fromId) : versions[0];
177
+ if (fromId && !from)
178
+ throw notFound();
179
+ const draft = await repository.save({
180
+ domain,
181
+ course,
182
+ number: (versions[0]?.number ?? 0) + 1,
183
+ status: course_version_1.CourseVersionStatus.DRAFT,
184
+ minSeconds: from?.minSeconds ?? 0,
185
+ acknowledgement: from?.acknowledgement ?? null,
186
+ passScore: from?.passScore ?? null,
187
+ questionCount: from?.questionCount ?? null,
188
+ attemptLimit: from?.attemptLimit ?? null,
189
+ sourceKind: from?.sourceKind ?? null,
190
+ sourceNote: from?.sourceNote ?? null,
191
+ creator: user,
192
+ updater: user
193
+ });
194
+ if (!from)
195
+ return draft;
196
+ for (const unit of await unitsOf(tx, from.id)) {
197
+ const copy = await tx.getRepository(course_unit_1.CourseUnit).save({
198
+ domain,
199
+ version: draft,
200
+ seq: unit.seq,
201
+ kind: unit.kind,
202
+ name: unit.name,
203
+ body: unit.body,
204
+ url: unit.url,
205
+ durationSeconds: unit.durationSeconds,
206
+ minSeconds: unit.minSeconds,
207
+ creator: user,
208
+ updater: user
209
+ });
210
+ const file = await tx.getRepository(attachment_base_1.Attachment).findOne({ where: { domain: { id: domain.id }, refType: exports.COURSE_UNIT_REF_TYPE, refBy: unit.id } });
211
+ if (file)
212
+ await copyFile(file, copy.id, context);
213
+ }
214
+ for (const question of await questionsOf(tx, from.id)) {
215
+ await tx.getRepository(course_question_1.CourseQuestion).save({
216
+ domain,
217
+ version: draft,
218
+ seq: question.seq,
219
+ kind: question.kind,
220
+ text: question.text,
221
+ choices: question.choices,
222
+ explanation: question.explanation,
223
+ drafted: question.drafted,
224
+ creator: user,
225
+ updater: user
226
+ });
227
+ }
228
+ return draft;
229
+ }
230
+ async function updateCourseVersion(id, patch, context) {
231
+ const { domain, user, tx } = context.state;
232
+ const version = await draftOf(tx, domain, id);
233
+ const next = { ...version, ...Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)) };
234
+ refuse((0, course_rules_1.versionInputRefusal)(next));
235
+ for (const field of ['minSeconds', 'passScore', 'questionCount', 'attemptLimit', 'sourceKind'])
236
+ if (patch[field] !== undefined)
237
+ version[field] = patch[field];
238
+ if (patch.minSeconds === null)
239
+ version.minSeconds = 0;
240
+ if (patch.acknowledgement !== undefined)
241
+ version.acknowledgement = text(patch.acknowledgement);
242
+ if (patch.sourceNote !== undefined)
243
+ version.sourceNote = text(patch.sourceNote);
244
+ version.updater = user;
245
+ return await tx.getRepository(course_version_1.CourseVersion).save(version);
246
+ }
247
+ /**
248
+ * The units of a draft, in the order given - new ones made, the ones given changed, the ones left out deleted with
249
+ * their files.
250
+ */
251
+ async function saveCourseUnits(versionId, units, context) {
252
+ const { domain, user, tx } = context.state;
253
+ const version = await draftOf(tx, domain, versionId);
254
+ for (const unit of units)
255
+ refuse((0, course_rules_1.unitInputRefusal)(unit));
256
+ const repository = tx.getRepository(course_unit_1.CourseUnit);
257
+ const existing = await unitsOf(tx, version.id);
258
+ const kept = new Set(units.map(one => one.id).filter(Boolean));
259
+ if ([...kept].some(id => !existing.some(one => one.id === id)))
260
+ throw notFound();
261
+ const gone = existing.filter(one => !kept.has(one.id));
262
+ if (gone.length) {
263
+ await (0, attachment_base_1.deleteAttachmentsByRef)(null, { refBys: gone.map(one => one.id), refType: exports.COURSE_UNIT_REF_TYPE }, context);
264
+ await repository.softDelete({ id: (0, typeorm_1.In)(gone.map(one => one.id)) });
265
+ }
266
+ const saved = [];
267
+ for (const [index, unit] of units.entries()) {
268
+ const row = unit.id ? existing.find(one => one.id === unit.id) : repository.create({ domain, version, creator: user });
269
+ Object.assign(row, {
270
+ seq: index + 1,
271
+ kind: unit.kind,
272
+ name: unit.name.trim(),
273
+ body: unit.kind === course_unit_1.CourseUnitKind.MARKDOWN ? unit.body : null,
274
+ url: unit.kind === course_unit_1.CourseUnitKind.LINK ? unit.url?.trim() : null,
275
+ durationSeconds: unit.kind === course_unit_1.CourseUnitKind.VIDEO ? (unit.durationSeconds ?? row.durationSeconds ?? null) : null,
276
+ minSeconds: unit.kind === course_unit_1.CourseUnitKind.PDF || unit.kind === course_unit_1.CourseUnitKind.MARKDOWN ? (unit.minSeconds ?? null) : null,
277
+ updater: user
278
+ });
279
+ saved.push(await repository.save(row));
280
+ }
281
+ return saved;
282
+ }
283
+ /** A video's or a PDF's file for a unit of a draft - it replaces the one before. A video's length comes with it. */
284
+ async function attachCourseUnitFile(unitId, file, durationSeconds, context) {
285
+ const { domain, user, tx } = context.state;
286
+ const unit = await tx.getRepository(course_unit_1.CourseUnit).findOne({ where: { id: unitId, domain: { id: domain.id } } });
287
+ if (!unit)
288
+ throw notFound();
289
+ await draftOf(tx, domain, unit.versionId);
290
+ if (unit.kind !== course_unit_1.CourseUnitKind.VIDEO && unit.kind !== course_unit_1.CourseUnitKind.PDF)
291
+ throw new shell_1.Refusal('learning-unit-file-not-taken', {}, 'only a video or a PDF unit takes a file');
292
+ await (0, attachment_base_1.deleteAttachmentsByRef)(null, { refBys: [unit.id], refType: exports.COURSE_UNIT_REF_TYPE }, context);
293
+ await (0, attachment_base_1.createAttachment)(null, { attachment: { file, refType: exports.COURSE_UNIT_REF_TYPE, refBy: unit.id } }, context);
294
+ if (unit.kind === course_unit_1.CourseUnitKind.VIDEO && durationSeconds != null) {
295
+ refuse((0, course_rules_1.unitInputRefusal)({ kind: unit.kind, name: unit.name, durationSeconds: Math.round(durationSeconds) }));
296
+ unit.durationSeconds = Math.round(durationSeconds);
297
+ }
298
+ unit.updater = user;
299
+ return await tx.getRepository(course_unit_1.CourseUnit).save(unit);
300
+ }
301
+ /** The choices kept with their ids - an id given stays, a new choice gets one. */
302
+ const choicesOf = (input) => input.choices.map(one => ({ id: one.id || (0, crypto_1.randomUUID)(), text: one.text.trim(), correct: !!one.correct }));
303
+ /**
304
+ * The questions of a draft, in the order given - saved by a manager, so each is looked over (`drafted` off). The ones
305
+ * left out are deleted.
306
+ */
307
+ async function saveCourseQuestions(versionId, questions, context) {
308
+ const { domain, user, tx } = context.state;
309
+ const version = await draftOf(tx, domain, versionId);
310
+ for (const question of questions)
311
+ refuse((0, course_rules_1.questionInputRefusal)(question));
312
+ const repository = tx.getRepository(course_question_1.CourseQuestion);
313
+ const existing = await questionsOf(tx, version.id);
314
+ const kept = new Set(questions.map(one => one.id).filter(Boolean));
315
+ if ([...kept].some(id => !existing.some(one => one.id === id)))
316
+ throw notFound();
317
+ const gone = existing.filter(one => !kept.has(one.id));
318
+ if (gone.length)
319
+ await repository.softDelete({ id: (0, typeorm_1.In)(gone.map(one => one.id)) });
320
+ const saved = [];
321
+ for (const [index, question] of questions.entries()) {
322
+ const row = question.id ? existing.find(one => one.id === question.id) : repository.create({ domain, version, creator: user });
323
+ Object.assign(row, { seq: index + 1, kind: question.kind, text: question.text.trim(), choices: choicesOf(question), explanation: text(question.explanation), drafted: false, updater: user });
324
+ saved.push(await repository.save(row));
325
+ }
326
+ return saved;
327
+ }
328
+ /** Questions the assistant drafted, added at the end of a draft - each `drafted` until a manager looks it over (decision 12). */
329
+ async function addDraftedQuestions(versionId, questions, context) {
330
+ const { domain, user, tx } = context.state;
331
+ const version = await draftOf(tx, domain, versionId);
332
+ const valid = questions.filter(one => !(0, course_rules_1.questionInputRefusal)(one));
333
+ const last = (await questionsOf(tx, version.id)).reduce((max, one) => Math.max(max, one.seq), 0);
334
+ const saved = [];
335
+ for (const [index, question] of valid.entries()) {
336
+ saved.push(await tx.getRepository(course_question_1.CourseQuestion).save({
337
+ domain,
338
+ version,
339
+ seq: last + index + 1,
340
+ kind: question.kind,
341
+ text: question.text.trim(),
342
+ choices: choicesOf(question),
343
+ explanation: text(question.explanation),
344
+ drafted: true,
345
+ creator: user,
346
+ updater: user
347
+ }));
348
+ }
349
+ return saved;
350
+ }
351
+ /**
352
+ * Publishes a draft: what it says is checked (decisions 2-4), it is claimed by an update that only a draft matches -
353
+ * two at once publish once - and the version published before closes. Enrolments in progress end on their own version.
354
+ */
355
+ async function publishCourseVersion(id, context) {
356
+ const { domain, user, tx } = context.state;
357
+ const version = await versionOf(tx, domain, id);
358
+ const units = await unitsOf(tx, version.id);
359
+ const files = units.length
360
+ ? await tx.getRepository(attachment_base_1.Attachment).find({ where: { domain: { id: domain.id }, refType: exports.COURSE_UNIT_REF_TYPE, refBy: (0, typeorm_1.In)(units.map(one => one.id)) } })
361
+ : [];
362
+ const questions = await questionsOf(tx, version.id);
363
+ refuse((0, course_rules_1.publishRefusal)({ version, units: units.map(one => ({ ...one, hasFile: files.some(file => file.refBy === one.id) })), questions }));
364
+ const now = new Date();
365
+ const claimed = await tx.getRepository(course_version_1.CourseVersion).update({ id: version.id, status: course_version_1.CourseVersionStatus.DRAFT }, { status: course_version_1.CourseVersionStatus.PUBLISHED, publishedAt: now, updater: user });
366
+ if (!claimed.affected)
367
+ throw new shell_1.Refusal('learning-version-not-draft', {}, 'the version is not a draft any more');
368
+ await tx
369
+ .getRepository(course_version_1.CourseVersion)
370
+ .update({ domain: { id: domain.id }, course: { id: version.courseId }, status: course_version_1.CourseVersionStatus.PUBLISHED, id: (0, typeorm_1.Not)(version.id) }, { status: course_version_1.CourseVersionStatus.CLOSED, closedAt: now, updater: user });
371
+ return await versionOf(tx, domain, version.id);
372
+ }
373
+ /** Closes the published version - nobody starts it any more; those who started end on it. */
374
+ async function closeCourseVersion(id, context) {
375
+ const { domain, user, tx } = context.state;
376
+ const version = await versionOf(tx, domain, id);
377
+ const claimed = await tx.getRepository(course_version_1.CourseVersion).update({ id: version.id, status: course_version_1.CourseVersionStatus.PUBLISHED }, { status: course_version_1.CourseVersionStatus.CLOSED, closedAt: new Date(), updater: user });
378
+ if (!claimed.affected)
379
+ throw new shell_1.Refusal('learning-version-not-published', {}, 'only the published version is closed');
380
+ return await versionOf(tx, domain, version.id);
381
+ }
382
+ /** A draft no learner saw is deleted with its units, their files and its questions. */
383
+ async function deleteCourseVersion(id, context) {
384
+ const { domain, tx } = context.state;
385
+ const version = await draftOf(tx, domain, id);
386
+ const units = await unitsOf(tx, version.id);
387
+ if (units.length) {
388
+ await (0, attachment_base_1.deleteAttachmentsByRef)(null, { refBys: units.map(one => one.id), refType: exports.COURSE_UNIT_REF_TYPE }, context);
389
+ await tx.getRepository(course_unit_1.CourseUnit).softDelete({ id: (0, typeorm_1.In)(units.map(one => one.id)) });
390
+ }
391
+ await tx.getRepository(course_question_1.CourseQuestion).softDelete({ version: { id: version.id } });
392
+ await tx.getRepository(course_version_1.CourseVersion).softDelete({ id: version.id });
393
+ return true;
394
+ }
395
+ /** The file of each unit given - for a video's or a PDF's address. */
396
+ async function unitFilesOf(domain, unitIds) {
397
+ if (!unitIds.length)
398
+ return new Map();
399
+ const rows = await (0, shell_1.getRepository)(attachment_base_1.Attachment).find({ where: { domain: { id: domain.id }, refType: exports.COURSE_UNIT_REF_TYPE, refBy: (0, typeorm_1.In)(unitIds) } });
400
+ return new Map(rows.map(one => [one.refBy, one]));
401
+ }
402
+ //# sourceMappingURL=course-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"course-service.js","sourceRoot":"","sources":["../../../server/service/course/course-service.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What the doors of courses do with the database (ADR-0194 decisions 2-4): a course, its versions written as drafts,\n * their units and questions, publishing one (the one before closes) and copying one into the next draft. Nothing here\n * changes a published version.\n */\nimport { randomUUID } from 'crypto'\nimport { Readable } from 'stream'\nimport { EntityManager, In, Not } from 'typeorm'\n\nimport { Attachment, createAttachment, deleteAttachmentsByRef, STORAGE } from '@things-factory/attachment-base'\nimport { checkPermission, User } from '@things-factory/auth-base'\nimport { getRepository, Refusal } from '@things-factory/shell'\n\nimport { Course } from './course'\nimport { CourseChoice, CourseQuestion, CourseQuestionKind } from './course-question'\nimport { publishRefusal, questionInputRefusal, unitInputRefusal, versionEditRefusal, versionInputRefusal } from './course-rules'\nimport { CourseUnit, CourseUnitKind } from './course-unit'\nimport { CourseVersion, CourseVersionStatus } from './course-version'\nimport { CoursePatch, CourseQuestionInput, CourseUnitInput, CourseVersionPatch, NewCourse } from './course-type'\n\n/** A unit's file hangs on it under this `refType` (attachment-base). */\nexport const COURSE_UNIT_REF_TYPE = 'CourseUnit'\n\nconst text = (value: unknown) => (typeof value === 'string' && value.trim() ? value.trim() : null)\n\n/** The request's transaction, or the data source's manager for a read that runs without one. */\nexport function learningManagerOf(context: any): EntityManager {\n return context?.state?.tx ?? getRepository(Course).manager\n}\n\n/** Whether the request holds `category` of this module (`course`, `learning`) - asked once per request. */\nexport async function holds(context: any, category: 'course' | 'learning', privilege: 'query' | 'mutation' = 'mutation'): Promise<boolean> {\n const state = context?.state ?? {}\n const { domain, user, unsafeIP, prohibitedPrivileges } = state\n if (!domain || !user) return false\n const key = `__learning:${category}:${privilege}`\n state[key] ??= checkPermission({ category, privilege, owner: true } as any, user, domain, unsafeIP, prohibitedPrivileges).then(Boolean)\n return await state[key]\n}\n\n/**\n * Whether the request may read what a version says - its units and their files: a manager (`course` query) any version;\n * anyone else only the version an enrolment of theirs shows - its own once started, the published one before. Asked on\n * every path to a version, not only the top-level reads: a learner reached a draft through\n * `myEnrollment → course → versions → units` (2026-11-08).\n */\nexport async function mayReadVersion(context: any, version: { id: string; courseId?: string; status: string } | null | undefined): Promise<boolean> {\n if (!version) return false\n if (await holds(context, 'course', 'query')) return true\n const { domain, user } = context?.state ?? {}\n if (!domain || !user) return false\n const state = context.state\n state.__learningShown ??= learningManagerOf(context)\n .getRepository('Enrollment')\n .find({ where: { domain: { id: domain.id }, user: { id: user.id }, status: In(['ASSIGNED', 'IN_PROGRESS', 'COMPLETED']) } as any, relations: ['course', 'version'] })\n const mine: any[] = await state.__learningShown\n return mine.some(one => one.course?.id === version.courseId && (one.version ? one.version.id === version.id : version.status === CourseVersionStatus.PUBLISHED))\n}\n\nconst notFound = () => new Refusal('learning-not-found', {}, 'the record was not found. reload and try again.')\n\nfunction refuse(code: string | null): void {\n if (code) throw new Refusal(code, {}, `refused (${code})`)\n}\n\n/* ---------------------------------------------------------------- courses */\n\nexport async function courseOf(tx: EntityManager, domain: { id: string }, id: string): Promise<Course> {\n const row = id ? await tx.getRepository(Course).findOne({ where: { id, domain: { id: domain.id } } as any }) : null\n if (!row) throw notFound()\n return row\n}\n\nasync function managerOf(tx: EntityManager, domain: any, managerId: string | null | undefined, fallback: User): Promise<User> {\n if (!managerId) return fallback\n const user = await tx.getRepository(User).findOne({ where: { id: managerId }, relations: ['domains'] })\n if (!user || !(user.domains ?? []).some(one => one.id === domain.id)) throw new Refusal('learning-manager-unknown', {}, 'the manager must be a user of this domain')\n return user\n}\n\nexport async function createCourse(input: NewCourse, context: any): Promise<Course> {\n const { domain, user, tx } = context.state\n const name = text(input.name)\n if (!name) throw new Refusal('learning-course-name-required', {}, 'give the course a name')\n if (await tx.getRepository(Course).findOne({ where: { domain: { id: domain.id }, name } as any })) throw new Refusal('learning-course-name-taken', { name }, `a course named ${name} exists`)\n return await tx.getRepository(Course).save({\n domain,\n name,\n description: text(input.description),\n category: text(input.category),\n manager: await managerOf(tx, domain, input.managerId, user),\n creator: user,\n updater: user\n } as any)\n}\n\nexport async function updateCourse(id: string, patch: CoursePatch, context: any): Promise<Course> {\n const { domain, user, tx } = context.state\n const row = await courseOf(tx, domain, id)\n if (patch.name !== undefined) {\n const name = text(patch.name)\n if (!name) throw new Refusal('learning-course-name-required', {}, 'give the course a name')\n const taken = await tx.getRepository(Course).findOne({ where: { domain: { id: domain.id }, name, id: Not(id) } as any })\n if (taken) throw new Refusal('learning-course-name-taken', { name }, `a course named ${name} exists`)\n row.name = name\n }\n if (patch.description !== undefined) row.description = text(patch.description)\n if (patch.category !== undefined) row.category = text(patch.category)\n if (patch.managerId !== undefined) row.manager = await managerOf(tx, domain, patch.managerId, user)\n row.updater = user\n return await tx.getRepository(Course).save(row)\n}\n\n/* ---------------------------------------------------------------- versions */\n\nexport async function versionOf(tx: EntityManager, domain: { id: string }, id: string): Promise<CourseVersion> {\n const row = id ? await tx.getRepository(CourseVersion).findOne({ where: { id, domain: { id: domain.id } } as any }) : null\n if (!row) throw notFound()\n return row\n}\n\n/** The version learners take now - the one published, or none. */\nexport async function publishedVersionOf(tx: EntityManager, domain: { id: string }, courseId: string): Promise<CourseVersion | null> {\n return await tx.getRepository(CourseVersion).findOne({ where: { domain: { id: domain.id }, course: { id: courseId }, status: CourseVersionStatus.PUBLISHED } as any })\n}\n\nexport async function unitsOf(tx: EntityManager, versionId: string): Promise<CourseUnit[]> {\n return await tx.getRepository(CourseUnit).find({ where: { version: { id: versionId } } as any, order: { seq: 'ASC' } as any })\n}\n\nexport async function questionsOf(tx: EntityManager, versionId: string): Promise<CourseQuestion[]> {\n return await tx.getRepository(CourseQuestion).find({ where: { version: { id: versionId } } as any, order: { seq: 'ASC' } as any })\n}\n\n/** A draft version to write in, refused while the version is published or closed (decision 2). */\nasync function draftOf(tx: EntityManager, domain: { id: string }, id: string): Promise<CourseVersion> {\n const version = await versionOf(tx, domain, id)\n refuse(versionEditRefusal(version))\n return version\n}\n\n/** The file of a unit, copied into a new file of another unit - each version keeps its own. */\nasync function copyFile(from: Attachment, toUnitId: string, context: any): Promise<void> {\n const bytes: Buffer = Buffer.from(await STORAGE.readFile(from.path))\n const file = Promise.resolve({ filename: from.name, mimetype: from.mimetype, encoding: from.encoding, createReadStream: () => Readable.from([bytes]) })\n await createAttachment(null, { attachment: { file, refType: COURSE_UNIT_REF_TYPE, refBy: toUnitId } } as any, context)\n}\n\n/**\n * A new draft of a course - the next number, with what `fromId` says copied (the latest version when not given): its\n * settings, units with their files, and questions. One draft at a time.\n */\nexport async function createCourseVersion(courseId: string, fromId: string | null | undefined, context: any): Promise<CourseVersion> {\n const { domain, user, tx } = context.state\n const course = await courseOf(tx, domain, courseId)\n const repository = (tx as EntityManager).getRepository(CourseVersion)\n const versions = await repository.find({ where: { domain: { id: domain.id }, course: { id: course.id } } as any, order: { number: 'DESC' } as any })\n if (versions.some(one => one.status === CourseVersionStatus.DRAFT)) throw new Refusal('learning-draft-exists', {}, 'the course has a draft version - write in it')\n const from = fromId ? versions.find(one => one.id === fromId) : versions[0]\n if (fromId && !from) throw notFound()\n\n const draft = await repository.save({\n domain,\n course,\n number: (versions[0]?.number ?? 0) + 1,\n status: CourseVersionStatus.DRAFT,\n minSeconds: from?.minSeconds ?? 0,\n acknowledgement: from?.acknowledgement ?? null,\n passScore: from?.passScore ?? null,\n questionCount: from?.questionCount ?? null,\n attemptLimit: from?.attemptLimit ?? null,\n sourceKind: from?.sourceKind ?? null,\n sourceNote: from?.sourceNote ?? null,\n creator: user,\n updater: user\n } as any)\n if (!from) return draft\n\n for (const unit of await unitsOf(tx, from.id)) {\n const copy = await tx.getRepository(CourseUnit).save({\n domain,\n version: draft,\n seq: unit.seq,\n kind: unit.kind,\n name: unit.name,\n body: unit.body,\n url: unit.url,\n durationSeconds: unit.durationSeconds,\n minSeconds: unit.minSeconds,\n creator: user,\n updater: user\n } as any)\n const file = await tx.getRepository(Attachment).findOne({ where: { domain: { id: domain.id }, refType: COURSE_UNIT_REF_TYPE, refBy: unit.id } as any })\n if (file) await copyFile(file, copy.id, context)\n }\n for (const question of await questionsOf(tx, from.id)) {\n await tx.getRepository(CourseQuestion).save({\n domain,\n version: draft,\n seq: question.seq,\n kind: question.kind,\n text: question.text,\n choices: question.choices,\n explanation: question.explanation,\n drafted: question.drafted,\n creator: user,\n updater: user\n } as any)\n }\n return draft\n}\n\nexport async function updateCourseVersion(id: string, patch: CourseVersionPatch, context: any): Promise<CourseVersion> {\n const { domain, user, tx } = context.state\n const version = await draftOf(tx, domain, id)\n const next = { ...version, ...Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)) } as CourseVersion\n refuse(versionInputRefusal(next))\n for (const field of ['minSeconds', 'passScore', 'questionCount', 'attemptLimit', 'sourceKind'] as const) if (patch[field] !== undefined) (version as any)[field] = patch[field]\n if (patch.minSeconds === null) version.minSeconds = 0\n if (patch.acknowledgement !== undefined) version.acknowledgement = text(patch.acknowledgement)\n if (patch.sourceNote !== undefined) version.sourceNote = text(patch.sourceNote)\n version.updater = user\n return await tx.getRepository(CourseVersion).save(version)\n}\n\n/**\n * The units of a draft, in the order given - new ones made, the ones given changed, the ones left out deleted with\n * their files.\n */\nexport async function saveCourseUnits(versionId: string, units: CourseUnitInput[], context: any): Promise<CourseUnit[]> {\n const { domain, user, tx } = context.state\n const version = await draftOf(tx, domain, versionId)\n for (const unit of units) refuse(unitInputRefusal(unit))\n const repository = (tx as EntityManager).getRepository(CourseUnit)\n const existing = await unitsOf(tx, version.id)\n const kept = new Set(units.map(one => one.id).filter(Boolean))\n if ([...kept].some(id => !existing.some(one => one.id === id))) throw notFound()\n\n const gone = existing.filter(one => !kept.has(one.id))\n if (gone.length) {\n await deleteAttachmentsByRef(null, { refBys: gone.map(one => one.id), refType: COURSE_UNIT_REF_TYPE }, context)\n await repository.softDelete({ id: In(gone.map(one => one.id)) })\n }\n const saved: CourseUnit[] = []\n for (const [index, unit] of units.entries()) {\n const row: any = unit.id ? existing.find(one => one.id === unit.id) : repository.create({ domain, version, creator: user } as any)\n Object.assign(row, {\n seq: index + 1,\n kind: unit.kind,\n name: unit.name.trim(),\n body: unit.kind === CourseUnitKind.MARKDOWN ? unit.body : null,\n url: unit.kind === CourseUnitKind.LINK ? unit.url?.trim() : null,\n durationSeconds: unit.kind === CourseUnitKind.VIDEO ? (unit.durationSeconds ?? row.durationSeconds ?? null) : null,\n minSeconds: unit.kind === CourseUnitKind.PDF || unit.kind === CourseUnitKind.MARKDOWN ? (unit.minSeconds ?? null) : null,\n updater: user\n })\n saved.push(await repository.save(row))\n }\n return saved\n}\n\n/** A video's or a PDF's file for a unit of a draft - it replaces the one before. A video's length comes with it. */\nexport async function attachCourseUnitFile(unitId: string, file: any, durationSeconds: number | null | undefined, context: any): Promise<CourseUnit> {\n const { domain, user, tx } = context.state\n const unit = await (tx as EntityManager).getRepository(CourseUnit).findOne({ where: { id: unitId, domain: { id: domain.id } } as any })\n if (!unit) throw notFound()\n await draftOf(tx, domain, unit.versionId!)\n if (unit.kind !== CourseUnitKind.VIDEO && unit.kind !== CourseUnitKind.PDF) throw new Refusal('learning-unit-file-not-taken', {}, 'only a video or a PDF unit takes a file')\n await deleteAttachmentsByRef(null, { refBys: [unit.id], refType: COURSE_UNIT_REF_TYPE }, context)\n await createAttachment(null, { attachment: { file, refType: COURSE_UNIT_REF_TYPE, refBy: unit.id } } as any, context)\n if (unit.kind === CourseUnitKind.VIDEO && durationSeconds != null) {\n refuse(unitInputRefusal({ kind: unit.kind, name: unit.name, durationSeconds: Math.round(durationSeconds) }))\n unit.durationSeconds = Math.round(durationSeconds)\n }\n unit.updater = user\n return await tx.getRepository(CourseUnit).save(unit)\n}\n\n/** The choices kept with their ids - an id given stays, a new choice gets one. */\nconst choicesOf = (input: CourseQuestionInput): CourseChoice[] => input.choices.map(one => ({ id: one.id || randomUUID(), text: one.text.trim(), correct: !!one.correct }))\n\n/**\n * The questions of a draft, in the order given - saved by a manager, so each is looked over (`drafted` off). The ones\n * left out are deleted.\n */\nexport async function saveCourseQuestions(versionId: string, questions: CourseQuestionInput[], context: any): Promise<CourseQuestion[]> {\n const { domain, user, tx } = context.state\n const version = await draftOf(tx, domain, versionId)\n for (const question of questions) refuse(questionInputRefusal(question))\n const repository = (tx as EntityManager).getRepository(CourseQuestion)\n const existing = await questionsOf(tx, version.id)\n const kept = new Set(questions.map(one => one.id).filter(Boolean))\n if ([...kept].some(id => !existing.some(one => one.id === id))) throw notFound()\n const gone = existing.filter(one => !kept.has(one.id))\n if (gone.length) await repository.softDelete({ id: In(gone.map(one => one.id)) })\n const saved: CourseQuestion[] = []\n for (const [index, question] of questions.entries()) {\n const row: any = question.id ? existing.find(one => one.id === question.id) : repository.create({ domain, version, creator: user } as any)\n Object.assign(row, { seq: index + 1, kind: question.kind, text: question.text.trim(), choices: choicesOf(question), explanation: text(question.explanation), drafted: false, updater: user })\n saved.push(await repository.save(row))\n }\n return saved\n}\n\n/** Questions the assistant drafted, added at the end of a draft - each `drafted` until a manager looks it over (decision 12). */\nexport async function addDraftedQuestions(versionId: string, questions: CourseQuestionInput[], context: any): Promise<CourseQuestion[]> {\n const { domain, user, tx } = context.state\n const version = await draftOf(tx, domain, versionId)\n const valid = questions.filter(one => !questionInputRefusal(one))\n const last = (await questionsOf(tx, version.id)).reduce((max, one) => Math.max(max, one.seq), 0)\n const saved: CourseQuestion[] = []\n for (const [index, question] of valid.entries()) {\n saved.push(\n await tx.getRepository(CourseQuestion).save({\n domain,\n version,\n seq: last + index + 1,\n kind: question.kind as CourseQuestionKind,\n text: question.text.trim(),\n choices: choicesOf(question),\n explanation: text(question.explanation),\n drafted: true,\n creator: user,\n updater: user\n } as any)\n )\n }\n return saved\n}\n\n/**\n * Publishes a draft: what it says is checked (decisions 2-4), it is claimed by an update that only a draft matches -\n * two at once publish once - and the version published before closes. Enrolments in progress end on their own version.\n */\nexport async function publishCourseVersion(id: string, context: any): Promise<CourseVersion> {\n const { domain, user, tx } = context.state\n const version = await versionOf(tx, domain, id)\n const units = await unitsOf(tx, version.id)\n const files = units.length\n ? await tx.getRepository(Attachment).find({ where: { domain: { id: domain.id }, refType: COURSE_UNIT_REF_TYPE, refBy: In(units.map(one => one.id)) } as any })\n : []\n const questions = await questionsOf(tx, version.id)\n refuse(publishRefusal({ version, units: units.map(one => ({ ...one, hasFile: files.some(file => file.refBy === one.id) })), questions }))\n\n const now = new Date()\n const claimed = await tx.getRepository(CourseVersion).update({ id: version.id, status: CourseVersionStatus.DRAFT } as any, { status: CourseVersionStatus.PUBLISHED, publishedAt: now, updater: user } as any)\n if (!claimed.affected) throw new Refusal('learning-version-not-draft', {}, 'the version is not a draft any more')\n await tx\n .getRepository(CourseVersion)\n .update({ domain: { id: domain.id }, course: { id: version.courseId }, status: CourseVersionStatus.PUBLISHED, id: Not(version.id) } as any, { status: CourseVersionStatus.CLOSED, closedAt: now, updater: user } as any)\n return await versionOf(tx, domain, version.id)\n}\n\n/** Closes the published version - nobody starts it any more; those who started end on it. */\nexport async function closeCourseVersion(id: string, context: any): Promise<CourseVersion> {\n const { domain, user, tx } = context.state\n const version = await versionOf(tx, domain, id)\n const claimed = await tx.getRepository(CourseVersion).update({ id: version.id, status: CourseVersionStatus.PUBLISHED } as any, { status: CourseVersionStatus.CLOSED, closedAt: new Date(), updater: user } as any)\n if (!claimed.affected) throw new Refusal('learning-version-not-published', {}, 'only the published version is closed')\n return await versionOf(tx, domain, version.id)\n}\n\n/** A draft no learner saw is deleted with its units, their files and its questions. */\nexport async function deleteCourseVersion(id: string, context: any): Promise<boolean> {\n const { domain, tx } = context.state\n const version = await draftOf(tx, domain, id)\n const units = await unitsOf(tx, version.id)\n if (units.length) {\n await deleteAttachmentsByRef(null, { refBys: units.map(one => one.id), refType: COURSE_UNIT_REF_TYPE }, context)\n await tx.getRepository(CourseUnit).softDelete({ id: In(units.map(one => one.id)) })\n }\n await tx.getRepository(CourseQuestion).softDelete({ version: { id: version.id } } as any)\n await tx.getRepository(CourseVersion).softDelete({ id: version.id })\n return true\n}\n\n/** The file of each unit given - for a video's or a PDF's address. */\nexport async function unitFilesOf(domain: { id: string }, unitIds: string[]): Promise<Map<string, Attachment>> {\n if (!unitIds.length) return new Map()\n const rows = await getRepository(Attachment).find({ where: { domain: { id: domain.id }, refType: COURSE_UNIT_REF_TYPE, refBy: In(unitIds) } as any })\n return new Map(rows.map(one => [one.refBy!, one]))\n}\n\n"]}
@@ -0,0 +1,55 @@
1
+ import { Course } from './course';
2
+ import { CourseQuestionKind } from './course-question';
3
+ import { CourseUnitKind } from './course-unit';
4
+ import { CourseSourceKind } from './course-version';
5
+ export declare class NewCourse {
6
+ name: string;
7
+ description?: string;
8
+ category?: string;
9
+ managerId?: string;
10
+ }
11
+ export declare class CoursePatch {
12
+ name?: string;
13
+ description?: string;
14
+ category?: string;
15
+ managerId?: string;
16
+ }
17
+ export declare class CourseVersionPatch {
18
+ minSeconds?: number;
19
+ acknowledgement?: string;
20
+ passScore?: number;
21
+ questionCount?: number;
22
+ attemptLimit?: number;
23
+ sourceKind?: CourseSourceKind;
24
+ sourceNote?: string;
25
+ }
26
+ export declare class CourseUnitInput {
27
+ id?: string;
28
+ kind: CourseUnitKind;
29
+ name: string;
30
+ body?: string;
31
+ url?: string;
32
+ durationSeconds?: number;
33
+ minSeconds?: number;
34
+ }
35
+ export declare class CourseChoiceInput {
36
+ id?: string;
37
+ text: string;
38
+ correct: boolean;
39
+ }
40
+ export declare class CourseQuestionInput {
41
+ id?: string;
42
+ kind: CourseQuestionKind;
43
+ text: string;
44
+ choices: CourseChoiceInput[];
45
+ explanation?: string;
46
+ }
47
+ export declare class CourseChoiceKey {
48
+ id: string;
49
+ text: string;
50
+ correct: boolean;
51
+ }
52
+ export declare class CourseList {
53
+ items: Course[];
54
+ total: number;
55
+ }