@hed-hog/lms 0.0.364 → 0.0.365

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 (218) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  6. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  7. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  8. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  9. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  10. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12.service.js +628 -0
  12. package/dist/course/course-export-scorm12.service.js.map +1 -0
  13. package/dist/course/course-export.service.d.ts +84 -0
  14. package/dist/course/course-export.service.d.ts.map +1 -0
  15. package/dist/course/course-export.service.js +237 -0
  16. package/dist/course/course-export.service.js.map +1 -0
  17. package/dist/course/course-structure.controller.d.ts +17 -9
  18. package/dist/course/course-structure.controller.d.ts.map +1 -1
  19. package/dist/course/course-structure.controller.js +17 -4
  20. package/dist/course/course-structure.controller.js.map +1 -1
  21. package/dist/course/course-structure.service.d.ts +12 -4
  22. package/dist/course/course-structure.service.d.ts.map +1 -1
  23. package/dist/course/course-structure.service.js +98 -23
  24. package/dist/course/course-structure.service.js.map +1 -1
  25. package/dist/course/course-video-hls.service.d.ts +57 -0
  26. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  27. package/dist/course/course-video-hls.service.js +767 -0
  28. package/dist/course/course-video-hls.service.js.map +1 -0
  29. package/dist/course/course.controller.d.ts +45 -13
  30. package/dist/course/course.controller.d.ts.map +1 -1
  31. package/dist/course/course.controller.js +40 -26
  32. package/dist/course/course.controller.js.map +1 -1
  33. package/dist/course/course.mcp-tools.js +1 -1
  34. package/dist/course/course.mcp-tools.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +11 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +6 -9
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +57 -48
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  44. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  48. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  52. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  54. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  55. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  56. package/dist/course/dto/create-course-export.dto.js +71 -0
  57. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  58. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  59. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  61. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  62. package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
  63. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  64. package/dist/course/lms-bulk-upload-automation.service.js +102 -8
  65. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  66. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  67. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  69. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  71. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.controller.js +43 -2
  73. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  74. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  75. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload.service.js +59 -6
  77. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  78. package/dist/course/lms-setting.controller.d.ts +2 -1
  79. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  80. package/dist/course/lms-setting.controller.js +4 -2
  81. package/dist/course/lms-setting.controller.js.map +1 -1
  82. package/dist/course/scorm12-schemas.d.ts +4 -0
  83. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  84. package/dist/course/scorm12-schemas.js +9 -0
  85. package/dist/course/scorm12-schemas.js.map +1 -0
  86. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  87. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  88. package/dist/enterprise/training/training-student.service.js +217 -4
  89. package/dist/enterprise/training/training-student.service.js.map +1 -1
  90. package/dist/evaluation/evaluation.service.d.ts +18 -0
  91. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  92. package/dist/evaluation/evaluation.service.js +125 -0
  93. package/dist/evaluation/evaluation.service.js.map +1 -1
  94. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  95. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  96. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  97. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  98. package/dist/exam/exam.module.d.ts.map +1 -1
  99. package/dist/exam/exam.module.js +2 -1
  100. package/dist/exam/exam.module.js.map +1 -1
  101. package/dist/exam/exam.service.d.ts +21 -0
  102. package/dist/exam/exam.service.d.ts.map +1 -1
  103. package/dist/exam/exam.service.js +80 -0
  104. package/dist/exam/exam.service.js.map +1 -1
  105. package/dist/exam/question.controller.d.ts +27 -0
  106. package/dist/exam/question.controller.d.ts.map +1 -0
  107. package/dist/exam/question.controller.js +53 -0
  108. package/dist/exam/question.controller.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  113. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  114. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  115. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  116. package/dist/lms-commerce-access.subscriber.js +74 -0
  117. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  118. package/dist/lms.module.d.ts.map +1 -1
  119. package/dist/lms.module.js +6 -5
  120. package/dist/lms.module.js.map +1 -1
  121. package/dist/platforma/platforma-video.service.d.ts +39 -0
  122. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  123. package/dist/platforma/platforma-video.service.js +301 -0
  124. package/dist/platforma/platforma-video.service.js.map +1 -0
  125. package/dist/platforma/platforma.controller.d.ts +95 -1
  126. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  127. package/dist/platforma/platforma.controller.js +160 -2
  128. package/dist/platforma/platforma.controller.js.map +1 -1
  129. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  130. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  131. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  132. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  133. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  134. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  135. package/dist/student-xp/student-xp.controller.js +24 -0
  136. package/dist/student-xp/student-xp.controller.js.map +1 -1
  137. package/dist/student-xp/student-xp.service.d.ts +16 -0
  138. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  139. package/dist/student-xp/student-xp.service.js +51 -1
  140. package/dist/student-xp/student-xp.service.js.map +1 -1
  141. package/hedhog/data/evaluation_topic.yaml +17 -0
  142. package/hedhog/data/menu.yaml +0 -17
  143. package/hedhog/data/queue_definition.yaml +48 -0
  144. package/hedhog/data/route.yaml +94 -124
  145. package/hedhog/data/setting_group.yaml +19 -19
  146. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  147. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  148. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  149. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  150. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
  151. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  152. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  153. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  154. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  155. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  156. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  157. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  158. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  159. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  160. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  161. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  162. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  163. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
  164. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  167. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
  168. package/hedhog/frontend/messages/en.json +26 -28
  169. package/hedhog/frontend/messages/pt.json +26 -28
  170. package/hedhog/table/course_export.yaml +62 -0
  171. package/package.json +13 -9
  172. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  173. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  174. package/src/course/course-export-scorm12.service.ts +668 -0
  175. package/src/course/course-export.service.ts +280 -0
  176. package/src/course/course-structure.controller.ts +14 -2
  177. package/src/course/course-structure.service.ts +100 -7
  178. package/src/course/course-video-hls.service.ts +946 -0
  179. package/src/course/course.controller.ts +33 -19
  180. package/src/course/course.mcp-tools.ts +1 -1
  181. package/src/course/course.module.ts +11 -0
  182. package/src/course/course.service.ts +73 -60
  183. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  184. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  185. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  186. package/src/course/dto/create-course-export.dto.ts +56 -0
  187. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  188. package/src/course/lms-bulk-upload-automation.service.ts +153 -6
  189. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  190. package/src/course/lms-bulk-upload.controller.ts +32 -2
  191. package/src/course/lms-bulk-upload.service.ts +70 -7
  192. package/src/course/lms-setting.controller.ts +4 -2
  193. package/src/course/scorm12-schemas.ts +9 -0
  194. package/src/enterprise/training/training-student.service.ts +221 -2
  195. package/src/evaluation/evaluation.service.ts +123 -0
  196. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  197. package/src/exam/exam.module.ts +2 -1
  198. package/src/exam/exam.service.ts +86 -0
  199. package/src/exam/question.controller.ts +28 -0
  200. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  201. package/src/lms-commerce-access.subscriber.ts +88 -0
  202. package/src/lms.module.ts +6 -5
  203. package/src/platforma/platforma-video.service.ts +346 -0
  204. package/src/platforma/platforma.controller.ts +95 -1
  205. package/src/platforma/platforma.service.ts +268 -268
  206. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  207. package/src/student-xp/student-xp.controller.ts +18 -2
  208. package/src/student-xp/student-xp.service.ts +84 -2
  209. package/hedhog/data/video_resolution_profile.yaml +0 -7
  210. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  211. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  212. package/hedhog/table/video_resolution_profile.yaml +0 -18
  213. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  214. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  215. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  216. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  217. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  218. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -1,4 +1,5 @@
1
1
  import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
2
3
  import { PrismaService } from '@hed-hog/api-prisma';
3
4
  import {
4
5
  FileService,
@@ -21,6 +22,7 @@ import { basename, extname, join } from 'path';
21
22
  import { Readable } from 'stream';
22
23
  import { pipeline } from 'stream/promises';
23
24
  import { CourseVideoConversionService } from './course-video-conversion.service';
25
+ import { CourseVideoHlsService } from './course-video-hls.service';
24
26
  import {
25
27
  LMS_BULK_UPLOAD_DOWNLOAD_ORIGINAL_JOB,
26
28
  LMS_BULK_UPLOAD_RECEIVE_VIDEO_COMMAND,
@@ -58,6 +60,8 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
58
60
  private readonly webhookIntegrationService: WebhookIntegrationService,
59
61
  @Inject(forwardRef(() => CourseVideoConversionService))
60
62
  private readonly courseVideoConversionService: CourseVideoConversionService,
63
+ @Inject(forwardRef(() => CourseVideoHlsService))
64
+ private readonly courseVideoHlsService: CourseVideoHlsService,
61
65
  ) {}
62
66
 
63
67
  onModuleInit(): void {
@@ -124,6 +128,15 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
124
128
  }
125
129
 
126
130
  if (!match) {
131
+ if (uploadItem) {
132
+ await this.prisma.$executeRawUnsafe(
133
+ `UPDATE "lms_bulk_upload_item"
134
+ SET "status" = 'lesson_not_found', "received_at" = $2, "updated_at" = $2
135
+ WHERE "id" = $1`,
136
+ uploadItem.id,
137
+ now,
138
+ );
139
+ }
127
140
  return {
128
141
  received: true,
129
142
  matched: false,
@@ -244,7 +257,7 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
244
257
  },
245
258
  );
246
259
 
247
- await this.courseVideoConversionService.enqueueConversion({
260
+ await this.courseVideoHlsService.enqueueHls({
248
261
  userId,
249
262
  courseId,
250
263
  sessionId,
@@ -252,6 +265,15 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
252
265
  originalFileId: uploaded.id,
253
266
  });
254
267
 
268
+ if (Number.isInteger(Number(payload.uploadItemId)) && Number(payload.uploadItemId) > 0) {
269
+ await this.prisma.$executeRawUnsafe(
270
+ `UPDATE "lms_bulk_upload_item"
271
+ SET "status" = 'done', "completed_at" = NOW(), "updated_at" = NOW()
272
+ WHERE "id" = $1`,
273
+ Number(payload.uploadItemId),
274
+ ).catch(() => undefined);
275
+ }
276
+
255
277
  return {
256
278
  success: true,
257
279
  queueJobId: job.id,
@@ -345,8 +367,9 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
345
367
 
346
368
  for (const course of courses ?? []) {
347
369
  for (const session of course.course_module ?? []) {
348
- const sessionCode = this.normalizeComparableText(this.formatCode('S', Number(session.order ?? 0)));
349
- if (sessionCode !== parsed.sessionCode) {
370
+ const sessionNumeric = parsed.sessionCode.replace(/[^0-9]/g, '');
371
+ const moduleNumeric = String(Math.max(Number(session.order ?? 0), 0)).padStart(2, '0');
372
+ if (!sessionNumeric || sessionNumeric !== moduleNumeric) {
350
373
  continue;
351
374
  }
352
375
 
@@ -377,16 +400,23 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
377
400
  }
378
401
 
379
402
  private parseFilename(fileName: string) {
380
- const base = this.normalizeComparableText(fileName.replace(/\.[^.]+$/, ''));
403
+ const withoutTimestamp = fileName.replace(/^\d+[-_]+/, '');
404
+ const base = this.normalizeComparableText(withoutTimestamp.replace(/\.[^.]+$/, ''));
381
405
  const parts = base.split('_').filter(Boolean);
382
406
  if (parts.length < 3) {
383
407
  return null;
384
408
  }
385
409
 
410
+ const RESOLUTION_SUFFIXES = new Set(['1080', '720', '480', '360', '240', '4k', '2k', 'uhd', 'fhd', 'hd', 'sd']);
411
+ let titleParts = parts.slice(2);
412
+ if (titleParts.length > 1 && RESOLUTION_SUFFIXES.has(titleParts[titleParts.length - 1])) {
413
+ titleParts = titleParts.slice(0, -1);
414
+ }
415
+
386
416
  return {
387
417
  sessionCode: this.normalizeComparableText(parts[0]),
388
418
  courseCode: this.normalizeComparableText(parts[1]),
389
- lessonTitle: this.normalizeComparableText(parts.slice(2).join('_')),
419
+ lessonTitle: this.normalizeComparableText(titleParts.join('_')),
390
420
  };
391
421
  }
392
422
 
@@ -399,8 +429,9 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
399
429
  : [];
400
430
  const record = records[0] ?? (raw as any).detail ?? raw;
401
431
  const bucket = this.firstString(
402
- (raw as any)?.bucket?.name,
403
432
  (raw as any)?.bucketName,
433
+ typeof (raw as any)?.bucket === 'string' ? (raw as any)?.bucket : undefined,
434
+ (raw as any)?.bucket?.name,
404
435
  record?.s3?.bucket?.name,
405
436
  record?.bucket?.name,
406
437
  record?.detail?.bucket?.name,
@@ -550,6 +581,122 @@ export class LmsBulkUploadAutomationService implements OnModuleInit {
550
581
  return `${prefix}${String(Math.max(order, 0)).padStart(2, '0')}`;
551
582
  }
552
583
 
584
+ async linkLessonManually(
585
+ itemId: number,
586
+ params: { courseId: number; sessionId: number; lessonId: number; userId: number },
587
+ ) {
588
+ const items = await this.prisma.$queryRawUnsafe<
589
+ Array<{ id: number; uploaded_key: string | null; file_name: string }>
590
+ >(
591
+ `SELECT i."id", i."uploaded_key", i."file_name"
592
+ FROM "lms_bulk_upload_item" i
593
+ WHERE i."id" = $1
594
+ LIMIT 1`,
595
+ itemId,
596
+ );
597
+
598
+ const item = items[0];
599
+ if (!item) {
600
+ throw new BadRequestException('Upload item not found.');
601
+ }
602
+
603
+ const lessonRows = await this.prisma.$queryRawUnsafe<Array<{ id: number }>>(
604
+ `SELECT cl."id"
605
+ FROM "course_lesson" cl
606
+ INNER JOIN "course_module" cm ON cm."id" = cl."course_module_id"
607
+ WHERE cl."id" = $1 AND cm."id" = $2 AND cm."course_id" = $3
608
+ LIMIT 1`,
609
+ params.lessonId,
610
+ params.sessionId,
611
+ params.courseId,
612
+ );
613
+
614
+ if (!lessonRows[0]) {
615
+ throw new BadRequestException('Lesson does not belong to the given course and module.');
616
+ }
617
+
618
+ const now = new Date();
619
+ await this.prisma.$executeRawUnsafe(
620
+ `UPDATE "lms_bulk_upload_item"
621
+ SET "matched_course_id" = $2,
622
+ "matched_session_id" = $3,
623
+ "matched_lesson_id" = $4,
624
+ "status" = 'received',
625
+ "updated_at" = $5
626
+ WHERE "id" = $1`,
627
+ itemId,
628
+ params.courseId,
629
+ params.sessionId,
630
+ params.lessonId,
631
+ now,
632
+ );
633
+
634
+ const bucket = await this.resolveStorageContext();
635
+ const key = String(item.uploaded_key ?? '').trim();
636
+
637
+ if (!key) {
638
+ throw new BadRequestException('Upload item does not have a stored S3 key yet.');
639
+ }
640
+
641
+ const job = await this.queueJobService.enqueue(
642
+ {
643
+ type: LMS_BULK_UPLOAD_DOWNLOAD_ORIGINAL_JOB,
644
+ queueName: LMS_BULK_UPLOAD_DOWNLOAD_ORIGINAL_JOB,
645
+ payload: {
646
+ bucket: bucket.bucket,
647
+ key,
648
+ fileName: item.file_name,
649
+ uploadItemId: itemId,
650
+ userId: params.userId,
651
+ courseId: params.courseId,
652
+ sessionId: params.sessionId,
653
+ lessonId: params.lessonId,
654
+ },
655
+ maxAttempts: 3,
656
+ sourceModule: 'lms',
657
+ sourceEntity: 'lms_bulk_upload_item',
658
+ sourceEntityId: String(itemId),
659
+ priority: -6,
660
+ },
661
+ params.userId,
662
+ );
663
+
664
+ return { success: true, queueJobId: job.id };
665
+ }
666
+
667
+ async getItemVideoPresignedUrl(itemId: number): Promise<{ url: string; expiresIn: number }> {
668
+ const items = await this.prisma.$queryRawUnsafe<
669
+ Array<{ uploaded_key: string | null }>
670
+ >(
671
+ `SELECT "uploaded_key" FROM "lms_bulk_upload_item" WHERE "id" = $1 LIMIT 1`,
672
+ itemId,
673
+ );
674
+
675
+ const key = String(items[0]?.uploaded_key ?? '').trim();
676
+ if (!key) {
677
+ throw new BadRequestException('Upload item does not have a stored S3 key.');
678
+ }
679
+
680
+ const storage = await this.resolveStorageContext();
681
+ const s3 = new S3Client({
682
+ region: storage.region,
683
+ credentials: {
684
+ accessKeyId: storage.accessKeyId,
685
+ secretAccessKey: storage.secretAccessKey,
686
+ sessionToken: storage.sessionToken,
687
+ },
688
+ });
689
+
690
+ const expiresIn = 3600;
691
+ const url = await getSignedUrl(
692
+ s3,
693
+ new GetObjectCommand({ Bucket: storage.bucket, Key: key }),
694
+ { expiresIn },
695
+ );
696
+
697
+ return { url, expiresIn };
698
+ }
699
+
553
700
  private safeJsonParse(value: string) {
554
701
  try {
555
702
  return JSON.parse(value);
@@ -3,6 +3,7 @@ import {
3
3
  BadRequestException,
4
4
  Inject,
5
5
  Injectable,
6
+ Logger,
6
7
  forwardRef,
7
8
  } from '@nestjs/common';
8
9
 
@@ -21,26 +22,36 @@ const BULK_UPLOAD_LAMBDA_TEMPLATE_CODE = `'use strict';
21
22
 
22
23
  const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3');
23
24
 
24
- const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
25
+ const s3 = new S3Client({ region: process.env.LMS_BULK_UPLOAD_REGION || 'us-east-1' });
25
26
 
26
27
  exports.handler = async (event) => {
27
- const record = event.Records[0];
28
- const bucket = record.s3.bucket.name;
28
+ const record = event && Array.isArray(event.Records) && event.Records[0];
29
+ if (!record) {
30
+ console.log('No S3 record in event. Skipping.', JSON.stringify(event));
31
+ return { statusCode: 200 };
32
+ }
33
+
34
+ const bucketName = record.s3.bucket.name;
29
35
  const key = decodeURIComponent(record.s3.object.key.replace(/\\+/g, ' '));
30
36
  const size = record.s3.object.size;
31
37
  const etag = record.s3.object.eTag;
32
38
  const eventTime = record.eventTime;
33
39
  const webhookUrl = process.env.WEBHOOK_URL;
34
40
 
41
+ if (!webhookUrl) {
42
+ console.error('WEBHOOK_URL is not set');
43
+ return { statusCode: 500 };
44
+ }
45
+
35
46
  let contentType = 'application/octet-stream';
36
47
  try {
37
- const head = await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
48
+ const head = await s3.send(new HeadObjectCommand({ Bucket: bucketName, Key: key }));
38
49
  contentType = head.ContentType || contentType;
39
50
  } catch (err) {
40
51
  console.error('HeadObject error:', err.message);
41
52
  }
42
53
 
43
- const payload = { bucket, key, size, etag, eventTime, contentType };
54
+ const payload = { bucketName, key, size, etag, eventTime, contentType };
44
55
 
45
56
  console.log('Sending webhook:', JSON.stringify(payload));
46
57
 
@@ -61,6 +72,8 @@ exports.handler = async (event) => {
61
72
 
62
73
  @Injectable()
63
74
  export class LmsBulkUploadInfraService {
75
+ private readonly logger = new Logger(LmsBulkUploadInfraService.name);
76
+
64
77
  constructor(
65
78
  @Inject(forwardRef(() => FileService))
66
79
  private readonly fileService: FileService,
@@ -76,8 +89,25 @@ export class LmsBulkUploadInfraService {
76
89
  );
77
90
  }
78
91
 
92
+ this.logger.log(
93
+ `syncDesktopUploadInfrastructure: fn=${lambdaFunctionName} region=${input.region} ` +
94
+ `webhookUrl=${input.webhookUrl} secretLen=${input.webhookSecret.length}`,
95
+ );
96
+
79
97
  await this.ensureLambdaExists(lambdaFunctionName, lambdaNameResolution.generatedFromBucket, input);
80
98
 
99
+ const zipFile = this.buildSingleFileZip('index.js', BULK_UPLOAD_LAMBDA_TEMPLATE_CODE);
100
+ await this.fileService.updateLambdaFunctionCode({
101
+ functionName: lambdaFunctionName,
102
+ region: input.region,
103
+ accessKeyId: input.accessKeyId,
104
+ secretAccessKey: input.secretAccessKey,
105
+ sessionToken: input.sessionToken,
106
+ zipFile,
107
+ });
108
+
109
+ this.logger.log(`syncDesktopUploadInfrastructure: code updated, now updating env vars`);
110
+
81
111
  const lambdaConfiguration =
82
112
  await this.fileService.updateLambdaEnvironmentVariables({
83
113
  functionName: lambdaFunctionName,
@@ -93,6 +123,10 @@ export class LmsBulkUploadInfraService {
93
123
  },
94
124
  });
95
125
 
126
+ this.logger.log(
127
+ `syncDesktopUploadInfrastructure: env vars update complete, fn=${lambdaConfiguration.functionName} arn=${lambdaConfiguration.functionArn}`,
128
+ );
129
+
96
130
  const lambdaArn =
97
131
  String(
98
132
  lambdaConfiguration.functionArn ??
@@ -239,7 +273,6 @@ export class LmsBulkUploadInfraService {
239
273
  environmentVariables: {
240
274
  WEBHOOK_URL: input.webhookUrl,
241
275
  WEBHOOK_SECRET: input.webhookSecret,
242
- AWS_REGION: input.region,
243
276
  LMS_BULK_UPLOAD_BUCKET: input.bucketName,
244
277
  LMS_BULK_UPLOAD_REGION: input.region,
245
278
  },
@@ -1,12 +1,16 @@
1
1
  import { Role, User } from '@hed-hog/api';
2
- import { Body, Controller, Get, Post, Query } from '@nestjs/common';
2
+ import { Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
3
3
  import { CleanupUploadHistoryDto } from './dto/cleanup-upload-history.dto';
4
+ import { LmsBulkUploadAutomationService } from './lms-bulk-upload-automation.service';
4
5
  import { LmsBulkUploadService } from './lms-bulk-upload.service';
5
6
 
6
7
  @Role()
7
8
  @Controller('lms/bulk-upload')
8
9
  export class LmsBulkUploadController {
9
- constructor(private readonly bulkUploadService: LmsBulkUploadService) {}
10
+ constructor(
11
+ private readonly bulkUploadService: LmsBulkUploadService,
12
+ private readonly automationService: LmsBulkUploadAutomationService,
13
+ ) {}
10
14
 
11
15
  @Get('settings')
12
16
  getSettings() {
@@ -96,8 +100,34 @@ export class LmsBulkUploadController {
96
100
  });
97
101
  }
98
102
 
103
+ @Post('check-uploaded')
104
+ checkUploaded(
105
+ @Body() payload: { files?: Array<{ fileName?: string; sizeBytes?: number }> },
106
+ ) {
107
+ return this.bulkUploadService.checkUploadedFiles(payload?.files ?? []);
108
+ }
109
+
99
110
  @Post('sessions/cleanup')
100
111
  cleanupSessions(@Body() payload: CleanupUploadHistoryDto) {
101
112
  return this.bulkUploadService.cleanupUploadHistory(payload ?? {});
102
113
  }
114
+
115
+ @Get('item/:id/video-url')
116
+ getItemVideoUrl(@Param('id', ParseIntPipe) itemId: number) {
117
+ return this.automationService.getItemVideoPresignedUrl(itemId);
118
+ }
119
+
120
+ @Post('item/:id/link-lesson')
121
+ linkLesson(
122
+ @User('id') userId: number,
123
+ @Param('id', ParseIntPipe) itemId: number,
124
+ @Body() payload: { courseId: number; sessionId: number; lessonId: number },
125
+ ) {
126
+ return this.automationService.linkLessonManually(itemId, {
127
+ courseId: Number(payload.courseId),
128
+ sessionId: Number(payload.sessionId),
129
+ lessonId: Number(payload.lessonId),
130
+ userId,
131
+ });
132
+ }
103
133
  }
@@ -45,7 +45,8 @@ type BulkUploadItemStatus =
45
45
  | 'received'
46
46
  | 'done'
47
47
  | 'error'
48
- | 'cancelled';
48
+ | 'cancelled'
49
+ | 'lesson_not_found';
49
50
 
50
51
  type StartBulkUploadPayload = {
51
52
  appName?: string;
@@ -243,6 +244,15 @@ export class LmsBulkUploadService {
243
244
  async setBulkUploadLambdaRoleArn(value: string | null | undefined) {
244
245
  const lambdaRoleArn = String(value ?? '').trim();
245
246
 
247
+ if (lambdaRoleArn) {
248
+ const iamRolePattern = /^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role\/?[a-zA-Z_0-9+=,.@\-_/]+$/;
249
+ if (!iamRolePattern.test(lambdaRoleArn)) {
250
+ throw new BadRequestException(
251
+ 'lambdaRoleArn must be an IAM Role ARN (arn:aws:iam::<account-id>:role/<role-name>). Lambda function ARNs are not valid here.',
252
+ );
253
+ }
254
+ }
255
+
246
256
  await this.settingService.setManySettings({
247
257
  setting: [
248
258
  {
@@ -271,21 +281,34 @@ export class LmsBulkUploadService {
271
281
  if (storageProfileId) {
272
282
  const profile = await this.prismaService.integration_profile.findUnique({
273
283
  where: { id: storageProfileId },
274
- select: {
275
- config: true,
276
- },
284
+ select: { config: true },
277
285
  });
278
286
 
279
287
  const config = (profile?.config as IntegrationProfileConfig | null) ?? null;
280
288
  bucketName = String(config?.bucket ?? '').trim();
281
289
  }
282
290
 
291
+ const webhook = await this.prismaService.webhook_integration.findFirst({
292
+ where: {
293
+ OR: [
294
+ { slug: LMS_BULK_UPLOAD_WEBHOOK_SLUG },
295
+ { slug: { startsWith: `${LMS_BULK_UPLOAD_WEBHOOK_SLUG}-` } },
296
+ ],
297
+ },
298
+ orderBy: { id: 'asc' },
299
+ select: { id: true, status: true, public_uuid: true },
300
+ });
301
+
302
+ const webhookConfigured =
303
+ webhook?.status === 'active' && Boolean(webhook?.public_uuid);
304
+
283
305
  return {
284
306
  storageProfileId,
285
307
  bucketName,
286
308
  lambdaRoleArn: String(settings['lms-bulk-upload-lambda-role-arn'] ?? '').trim(),
287
309
  sessionDurationSeconds:
288
310
  Number(settings['lms-bulk-upload-sts-duration-seconds'] || 3600) || 3600,
311
+ webhookConfigured,
289
312
  };
290
313
  }
291
314
 
@@ -433,6 +456,8 @@ export class LmsBulkUploadService {
433
456
  });
434
457
  }
435
458
 
459
+ // Always regenerate the token on every save so the Lambda env vars stay in sync
460
+ // with the latest webhook credentials.
436
461
  return this.webhookIntegrationService.regenerateToken(webhook.id);
437
462
  }
438
463
 
@@ -647,7 +672,7 @@ export class LmsBulkUploadService {
647
672
  args.push(`%${search}%`);
648
673
  const searchParam = `$${args.length}`;
649
674
  where.push(
650
- `(i."file_name" ILIKE ${searchParam} OR s."app_name" ILIKE ${searchParam} OR COALESCE(sender."name", '') ILIKE ${searchParam} OR CAST(s."user_id" AS TEXT) ILIKE ${searchParam} OR COALESCE(mc."title", '') ILIKE ${searchParam} OR COALESCE(ml."title", '') ILIKE ${searchParam})`,
675
+ `(i."file_name" ILIKE ${searchParam} OR s."app_name" ILIKE ${searchParam} OR COALESCE(sender."name", u."name", '') ILIKE ${searchParam} OR CAST(s."user_id" AS TEXT) ILIKE ${searchParam} OR COALESCE(mc."title", '') ILIKE ${searchParam} OR COALESCE(ml."title", '') ILIKE ${searchParam})`,
651
676
  );
652
677
  }
653
678
 
@@ -657,6 +682,7 @@ export class LmsBulkUploadService {
657
682
  `SELECT COUNT(*)::int AS total
658
683
  FROM "lms_bulk_upload_item" i
659
684
  JOIN "lms_bulk_upload_session" s ON s."id" = i."session_id"
685
+ LEFT JOIN "user" u ON u."id" = s."user_id"
660
686
  LEFT JOIN LATERAL (
661
687
  SELECT p."name"
662
688
  FROM "person_user" pu
@@ -717,7 +743,7 @@ export class LmsBulkUploadService {
717
743
  s."app_name",
718
744
  s."user_id",
719
745
  u."photo_id" AS user_photo_id,
720
- sender."name" AS user_name,
746
+ COALESCE(sender."name", u."name") AS user_name,
721
747
  s."status" AS session_status,
722
748
  s."started_at" AS session_started_at,
723
749
  mc."id" AS matched_course_id,
@@ -796,6 +822,42 @@ export class LmsBulkUploadService {
796
822
  };
797
823
  }
798
824
 
825
+ async checkUploadedFiles(files: Array<{ fileName?: string; sizeBytes?: number }>) {
826
+ const normalized = files
827
+ .map((f) => ({
828
+ fileName: String(f?.fileName ?? '').trim(),
829
+ sizeBytes: Math.max(0, Number(f?.sizeBytes ?? 0) || 0),
830
+ }))
831
+ .filter((f) => f.fileName.length > 0);
832
+
833
+ if (normalized.length === 0) return { uploaded: [] };
834
+
835
+ const conditions = normalized
836
+ .map((_, i) => `(file_name = $${i * 2 + 1} AND size_bytes = $${i * 2 + 2}::bigint)`)
837
+ .join(' OR ');
838
+ const args: Array<string | number> = normalized.flatMap((f) => [f.fileName, f.sizeBytes]);
839
+
840
+ const rows = await this.prismaService.$queryRawUnsafe<
841
+ Array<{ file_name: string; size_bytes: number; uploaded_at: Date | null }>
842
+ >(
843
+ `SELECT file_name, size_bytes,
844
+ MAX(COALESCE(received_at, completed_at, updated_at)) AS uploaded_at
845
+ FROM "lms_bulk_upload_item"
846
+ WHERE status IN ('received', 'done')
847
+ AND (${conditions})
848
+ GROUP BY file_name, size_bytes`,
849
+ ...args,
850
+ );
851
+
852
+ return {
853
+ uploaded: rows.map((row) => ({
854
+ fileName: row.file_name,
855
+ sizeBytes: Number(row.size_bytes),
856
+ uploadedAt: row.uploaded_at?.toISOString() ?? null,
857
+ })),
858
+ };
859
+ }
860
+
799
861
  async cleanupUploadHistory(payload: {
800
862
  statuses?: BulkUploadCleanupStatus[];
801
863
  timeWindow?: BulkUploadCleanupWindow;
@@ -923,7 +985,8 @@ export class LmsBulkUploadService {
923
985
  normalized === 'received' ||
924
986
  normalized === 'done' ||
925
987
  normalized === 'error' ||
926
- normalized === 'cancelled'
988
+ normalized === 'cancelled' ||
989
+ normalized === 'lesson_not_found'
927
990
  ) {
928
991
  return normalized;
929
992
  }
@@ -16,7 +16,8 @@ export class LmsSettingController {
16
16
  @Get()
17
17
  async getFeatureFlags() {
18
18
  const v = await this.settingService.getSettingValues([
19
- 'lms-video-conversion-enabled',
19
+ 'lms-hls-resolutions',
20
+ 'lms-hls-segment-duration',
20
21
  'lms-image-extraction-enabled',
21
22
  'lms-audio-transcription-enabled',
22
23
  'lms-youtube-provider-enabled',
@@ -39,7 +40,8 @@ export class LmsSettingController {
39
40
  }
40
41
 
41
42
  return {
42
- videoConversionEnabled: v['lms-video-conversion-enabled'] !== false,
43
+ hlsResolutions: typeof v['lms-hls-resolutions'] === 'string' ? v['lms-hls-resolutions'] : '480,720,1080',
44
+ hlsSegmentDuration: Number(v['lms-hls-segment-duration'] || 6) || 6,
43
45
  imageExtractionEnabled: v['lms-image-extraction-enabled'] !== false,
44
46
  transcriptionEnabled: v['lms-audio-transcription-enabled'] !== false,
45
47
  youtubeEnabled: v['lms-youtube-provider-enabled'] !== false,
@@ -0,0 +1,9 @@
1
+ // Schemas XSD de controle exigidos pela spec SCORM 1.2 (CAM) na raiz do pacote.
2
+ // Conteudo verbatim dos arquivos padrao ADL/IMS.
3
+
4
+ export const IMS_XML_XSD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!-- filename=ims_xml.xsd --><xsd:schema xmlns=\"http://www.w3.org/XML/1998/namespace\" targetNamespace=\"http://www.w3.org/XML/1998/namespace\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" elementFormDefault=\"qualified\">\t<!-- 2001-02-22 edited by Thomas Wason IMS Global Learning Consortium, Inc. -->\t<xsd:annotation>\t\t<xsd:documentation>In namespace-aware XML processors, the &quot;xml&quot; prefix is bound to the namespace name http://www.w3.org/XML/1998/namespace.</xsd:documentation>\t\t<xsd:documentation>Do not reference this file in XML instances</xsd:documentation> <xsd:documentation>Schawn Thropp: Changed the uriReference type to string type</xsd:documentation>\t</xsd:annotation>\t<xsd:attribute name=\"lang\" type=\"xsd:language\">\t\t<xsd:annotation>\t\t\t<xsd:documentation>Refers to universal XML 1.0 lang attribute</xsd:documentation>\t\t</xsd:annotation>\t</xsd:attribute>\t<xsd:attribute name=\"base\" type=\"xsd:string\">\t\t<xsd:annotation>\t\t\t<xsd:documentation>Refers to XML Base: http://www.w3.org/TR/xmlbase</xsd:documentation>\t\t</xsd:annotation>\t</xsd:attribute>\t<xsd:attribute name=\"link\" type=\"xsd:string\"/></xsd:schema>\r\n";
5
+
6
+ export const IMSCP_ROOTV1P1P2_XSD = "<?xml version=\"1.0\"?>\r\n\r\n<!-- edited with XML Spy v3.5 (http://www.xmlspy.com) by Thomas Wason (private) -->\r\n<!-- filename=ims_cp_rootv1p1p2.xsd -->\r\n<!-- Copyright (2) 2001 IMS Global Learning Consortium, Inc. -->\r\n<!-- edited by Thomas Wason -->\r\n<!-- Conforms to w3c http://www.w3.org/TR/xmlschema-1/ 2000-10-24-->\r\n\r\n<xsd:schema xmlns=\"http://www.imsproject.org/xsd/imscp_rootv1p1p2\" \r\n targetNamespace=\"http://www.imsproject.org/xsd/imscp_rootv1p1p2\" \r\n xmlns:xml=\"http://www.w3.org/XML/1998/namespace\" \r\n xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" \r\n elementFormDefault=\"unqualified\" version=\"IMS CP 1.1.2\">\r\n\r\n <!-- ******************** -->\r\n <!-- ** Change History ** -->\r\n <!-- ******************** -->\r\n <xsd:annotation>\r\n <xsd:documentation xml:lang=\"en\">DRAFT XSD for IMS Content Packaging version 1.1 DRAFT</xsd:documentation>\r\n <xsd:documentation> Copyright (c) 2001 IMS GLC, Inc. </xsd:documentation>\r\n <xsd:documentation>2000-04-21, Adjustments by T.D. Wason from CP 1.0.</xsd:documentation>\r\n <xsd:documentation>2001-02-22, T.D.Wason: Modify for 2000-10-24 XML-Schema version. Modified to support extension.</xsd:documentation>\r\n <xsd:documentation>2001-03-12, T.D.Wason: Change filename, target and meta-data namespaces and meta-data fielname. Add meta-data to itemType, fileType and organizationType.</xsd:documentation>\r\n <xsd:documentation>Do not define namespaces for xml in XML instances generated from this xsd.</xsd:documentation>\r\n <xsd:documentation>Imports IMS meta-data xsd, lower case element names. </xsd:documentation>\r\n <xsd:documentation>This XSD provides a reference to the IMS meta-data root element as imsmd:record</xsd:documentation>\r\n <xsd:documentation>If the IMS meta-data is to be used in the XML instance then the instance must define an IMS meta-data prefix with a namespace. The meta-data targetNamespace should be used. </xsd:documentation>\r\n <xsd:documentation>2001-03-20, Thor Anderson: Remove manifestref, change resourceref back to identifierref, change manifest back to contained by manifest. --Tom Wason: manifest may contain _none_ or more manifests.</xsd:documentation>\r\n <xsd:documentation>2001-04-13 Tom Wason: corrected attirbute name structure. Was misnamed type. </xsd:documentation>\r\n <xsd:documentation>2001-05-14 Schawn Thropp: Made all complexType extensible with the group.any</xsd:documentation>\r\n <xsd:documentation>Added the anyAttribute to all complexTypes. Changed the href attribute on the fileType and resourceType to xsd:string</xsd:documentation>\r\n <xsd:documentation>Changed the maxLength of the href, identifierref, parameters, structure attributes to match the Information model.</xsd:documentation>\r\n <xsd:documentation>2001-07-25 Schawn Thropp: Changed the namespace for the Schema of Schemas to the 5/2/2001 W3C XML Schema</xsd:documentation> \r\n <xsd:documentation>Recommendation. attributeGroup attr.imsmd deleted, was not used anywhere. Any attribute declarations that have</xsd:documentation>\r\n <xsd:documentation>use = \"default\" changed to use=\"optional\" - attr.structure.req.</xsd:documentation>\r\n <xsd:documentation>Any attribute declarations that have value=\"somevalue\" changed to default=\"somevalue\",</xsd:documentation>\r\n <xsd:documentation>attr.structure.req (hierarchical). Removed references to IMS MD Version 1.1.</xsd:documentation>\r\n <xsd:documentation>Modified attribute group \"attr.resourcetype.req\" to change use from optional</xsd:documentation>\r\n <xsd:documentation>to required to match the information model. As a result the default value also needed to be removed</xsd:documentation> \r\n <xsd:documentation>Name change for XSD. Changed to match version of CP Spec </xsd:documentation> \r\n </xsd:annotation>\r\n\r\n <xsd:annotation>\r\n <xsd:documentation>Inclusions and Imports</xsd:documentation>\r\n </xsd:annotation>\r\n\r\n <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" schemaLocation=\"ims_xml.xsd\"/>\r\n\r\n <xsd:annotation>\r\n <xsd:documentation>Attribute Declarations</xsd:documentation>\r\n </xsd:annotation>\r\n\r\n <!-- **************************** -->\r\n <!-- ** Attribute Declarations ** -->\r\n <!-- **************************** -->\r\n <xsd:attributeGroup name=\"attr.base\">\r\n <xsd:attribute ref=\"xml:base\" use=\"optional\"/>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.default\">\r\n <xsd:attribute name=\"default\" type=\"xsd:IDREF\" use=\"optional\"/>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.href\">\r\n <xsd:attribute name=\"href\" use=\"optional\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:anyURI\">\r\n <xsd:maxLength value=\"2000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.href.req\">\r\n <xsd:attribute name=\"href\" use=\"required\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:anyURI\">\r\n <xsd:maxLength value=\"2000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup> \r\n\r\n <xsd:attributeGroup name=\"attr.identifier.req\">\r\n <xsd:attribute name=\"identifier\" type=\"xsd:ID\" use=\"required\"/>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.identifier\">\r\n <xsd:attribute name=\"identifier\" type=\"xsd:ID\" use=\"optional\"/>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.isvisible\">\r\n <xsd:attribute name=\"isvisible\" type=\"xsd:boolean\" use=\"optional\"/>\r\n </xsd:attributeGroup>\r\n \r\n <xsd:attributeGroup name=\"attr.parameters\">\r\n <xsd:attribute name=\"parameters\" use=\"optional\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"1000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n \r\n <xsd:attributeGroup name=\"attr.identifierref\">\r\n <xsd:attribute name=\"identifierref\" use=\"optional\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"2000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n \r\n <xsd:attributeGroup name=\"attr.identifierref.req\">\r\n <xsd:attribute name=\"identifierref\" use=\"required\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"2000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n \r\n <xsd:attributeGroup name=\"attr.resourcetype.req\">\r\n <xsd:attribute name=\"type\" use=\"required\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"1000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.structure.req\">\r\n <xsd:attribute name=\"structure\" use=\"optional\" default=\"hierarchical\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"200\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:attributeGroup name=\"attr.version\">\r\n <xsd:attribute name=\"version\" use=\"optional\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"20\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:annotation>\r\n <xsd:documentation>element groups</xsd:documentation>\r\n </xsd:annotation>\r\n\r\n <xsd:group name=\"grp.any\">\r\n <xsd:annotation>\r\n <xsd:documentation>Any namespaced element from any namespace may be included within an &quot;any&quot; element. The namespace for the imported element must be defined in the instance, and the schema must be imported. </xsd:documentation>\r\n </xsd:annotation>\r\n <xsd:sequence>\r\n <xsd:any namespace=\"##other\" processContents=\"strict\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n </xsd:sequence>\r\n </xsd:group>\r\n\r\n <!-- ************************** -->\r\n <!-- ** Element Declarations ** -->\r\n <!-- ************************** -->\r\n\r\n <xsd:element name=\"dependency\" type=\"dependencyType\"/>\r\n <xsd:element name=\"file\" type=\"fileType\"/>\r\n <xsd:element name=\"item\" type=\"itemType\"/>\r\n <xsd:element name=\"manifest\" type=\"manifestType\"/>\r\n <xsd:element name=\"metadata\" type=\"metadataType\"/>\r\n <xsd:element name=\"organization\" type=\"organizationType\"/>\r\n <xsd:element name=\"organizations\" type=\"organizationsType\"/>\r\n <xsd:element name=\"resource\" type=\"resourceType\"/>\r\n <xsd:element name=\"resources\" type=\"resourcesType\"/>\r\n <xsd:element name=\"schema\" type=\"schemaType\"/>\r\n <xsd:element name=\"schemaversion\" type=\"schemaversionType\"/>\r\n <xsd:element name=\"title\" type=\"titleType\"/>\r\n\r\n <!-- ******************* -->\r\n <!-- ** Complex Types ** -->\r\n <!-- ******************* -->\r\n\r\n <!-- **************** -->\r\n <!-- ** dependency ** -->\r\n <!-- **************** -->\r\n <xsd:complexType name=\"dependencyType\">\r\n <xsd:sequence>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.identifierref.req\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ********** -->\r\n <!-- ** file ** -->\r\n <!-- ********** -->\r\n <xsd:complexType name=\"fileType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"metadata\" minOccurs=\"0\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.href.req\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ********** -->\r\n <!-- ** item ** -->\r\n <!-- ********** -->\r\n <xsd:complexType name=\"itemType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"title\" minOccurs=\"0\"/>\r\n <xsd:element ref=\"item\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:element ref=\"metadata\" minOccurs=\"0\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.identifier.req\"/>\r\n <xsd:attributeGroup ref=\"attr.identifierref\"/>\r\n <xsd:attributeGroup ref=\"attr.isvisible\"/>\r\n <xsd:attributeGroup ref=\"attr.parameters\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ************** -->\r\n <!-- ** manifest ** -->\r\n <!-- ************** -->\r\n <xsd:complexType name=\"manifestType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"metadata\" minOccurs=\"0\"/>\r\n <xsd:element ref=\"organizations\"/>\r\n <xsd:element ref=\"resources\"/>\r\n <xsd:element ref=\"manifest\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.identifier.req\"/>\r\n <xsd:attributeGroup ref=\"attr.version\"/>\r\n <xsd:attribute ref=\"xml:base\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ************** -->\r\n <!-- ** metadata ** -->\r\n <!-- ************** -->\r\n <xsd:complexType name=\"metadataType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"schema\" minOccurs=\"0\"/>\r\n <xsd:element ref=\"schemaversion\" minOccurs=\"0\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n </xsd:complexType>\r\n \r\n <!-- ******************* -->\r\n <!-- ** organizations ** -->\r\n <!-- ******************* -->\r\n <xsd:complexType name=\"organizationsType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"organization\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.default\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ****************** -->\r\n <!-- ** organization ** -->\r\n <!-- ****************** -->\r\n <xsd:complexType name=\"organizationType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"title\" minOccurs=\"0\"/>\r\n <xsd:element ref=\"item\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:element ref=\"metadata\" minOccurs=\"0\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.identifier.req\"/>\r\n <xsd:attributeGroup ref=\"attr.structure.req\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- *************** -->\r\n <!-- ** resources ** -->\r\n <!-- *************** -->\r\n <xsd:complexType name=\"resourcesType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"resource\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.base\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n \r\n <!-- ************** -->\r\n <!-- ** resource ** -->\r\n <!-- ************** -->\r\n <xsd:complexType name=\"resourceType\">\r\n <xsd:sequence>\r\n <xsd:element ref=\"metadata\" minOccurs=\"0\"/>\r\n <xsd:element ref=\"file\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:element ref=\"dependency\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\r\n <xsd:group ref=\"grp.any\"/>\r\n </xsd:sequence>\r\n <xsd:attributeGroup ref=\"attr.identifier.req\"/>\r\n <xsd:attributeGroup ref=\"attr.resourcetype.req\"/>\r\n <xsd:attributeGroup ref=\"attr.base\"/>\r\n <xsd:attributeGroup ref=\"attr.href\"/>\r\n <xsd:anyAttribute namespace=\"##other\" processContents=\"strict\"/>\r\n </xsd:complexType>\r\n\r\n <!-- ****************** -->\r\n <!-- ** Simple Types ** -->\r\n <!-- ****************** -->\r\n\r\n <!-- ************ -->\r\n <!-- ** schema ** -->\r\n <!-- ************ -->\r\n <xsd:simpleType name=\"schemaType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"100\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n \r\n <!-- ******************* -->\r\n <!-- ** schemaversion ** -->\r\n <!-- ******************* -->\r\n <xsd:simpleType name=\"schemaversionType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"20\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n \r\n <!-- *********** -->\r\n <!-- ** title ** -->\r\n <!-- *********** -->\r\n <xsd:simpleType name=\"titleType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"200\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n</xsd:schema>\r\n";
7
+
8
+ export const ADLCP_ROOTV1P2_XSD = "<?xml version=\"1.0\"?>\r\n<!-- filename=adlcp_rootv1p2.xsd -->\r\n<!-- Conforms to w3c http://www.w3.org/TR/xmlschema-1/ 2000-10-24-->\r\n\r\n<xsd:schema xmlns=\"http://www.adlnet.org/xsd/adlcp_rootv1p2\"\r\n targetNamespace=\"http://www.adlnet.org/xsd/adlcp_rootv1p2\"\r\n xmlns:xml=\"http://www.w3.org/XML/1998/namespace\"\r\n xmlns:imscp=\"http://www.imsproject.org/xsd/imscp_rootv1p1p2\"\r\n xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\r\n elementFormDefault=\"unqualified\"\r\n version=\"ADL Version 1.2\">\r\n\r\n <xsd:import namespace=\"http://www.imsproject.org/xsd/imscp_rootv1p1p2\"\r\n schemaLocation=\"imscp_rootv1p1p2.xsd\"/>\r\n\r\n <xsd:element name=\"location\" type=\"locationType\"/>\r\n <xsd:element name=\"prerequisites\" type=\"prerequisitesType\"/>\r\n <xsd:element name=\"maxtimeallowed\" type=\"maxtimeallowedType\"/>\r\n <xsd:element name=\"timelimitaction\" type=\"timelimitactionType\"/>\r\n <xsd:element name=\"datafromlms\" type=\"datafromlmsType\"/>\r\n <xsd:element name=\"masteryscore\" type=\"masteryscoreType\"/>\r\n\r\n\r\n <xsd:element name=\"schema\" type=\"newSchemaType\"/>\r\n <xsd:simpleType name=\"newSchemaType\">\r\n <xsd:restriction base=\"imscp:schemaType\">\r\n <xsd:enumeration value=\"ADL SCORM\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n <xsd:element name=\"schemaversion\" type=\"newSchemaversionType\"/>\r\n <xsd:simpleType name=\"newSchemaversionType\">\r\n <xsd:restriction base=\"imscp:schemaversionType\">\r\n <xsd:enumeration value=\"1.2\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n\r\n <xsd:attribute name=\"scormtype\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:enumeration value=\"asset\"/>\r\n <xsd:enumeration value=\"sco\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n\r\n <xsd:simpleType name=\"locationType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"2000\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n\r\n <xsd:complexType name=\"prerequisitesType\">\r\n <xsd:simpleContent>\r\n <xsd:extension base=\"prerequisiteStringType\">\r\n <xsd:attributeGroup ref=\"attr.prerequisitetype\"/>\r\n </xsd:extension>\r\n </xsd:simpleContent>\r\n </xsd:complexType>\r\n\r\n <xsd:attributeGroup name=\"attr.prerequisitetype\">\r\n <xsd:attribute name=\"type\" use=\"required\">\r\n <xsd:simpleType>\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:enumeration value=\"aicc_script\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n </xsd:attribute>\r\n </xsd:attributeGroup>\r\n\r\n <xsd:simpleType name=\"maxtimeallowedType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"13\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n <xsd:simpleType name=\"timelimitactionType\">\r\n <xsd:restriction base=\"stringType\">\r\n <xsd:enumeration value=\"exit,no message\"/>\r\n <xsd:enumeration value=\"exit,message\"/>\r\n <xsd:enumeration value=\"continue,no message\"/>\r\n <xsd:enumeration value=\"continue,message\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n <xsd:simpleType name=\"datafromlmsType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"255\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n <xsd:simpleType name=\"masteryscoreType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"200\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n <xsd:simpleType name=\"stringType\">\r\n <xsd:restriction base=\"xsd:string\"/>\r\n </xsd:simpleType>\r\n \r\n <xsd:simpleType name=\"prerequisiteStringType\">\r\n <xsd:restriction base=\"xsd:string\">\r\n <xsd:maxLength value=\"200\"/>\r\n </xsd:restriction>\r\n </xsd:simpleType>\r\n\r\n</xsd:schema>\r\n";
9
+