@learnpack/learnpack 5.0.319 → 5.0.322

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.
@@ -32,6 +32,7 @@ const creatorUtilities_2 = require("../utils/creatorUtilities");
32
32
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
33
33
  const publish_1 = require("./publish");
34
34
  const export_1 = require("../utils/export");
35
+ const errorHandler_1 = require("../utils/errorHandler");
35
36
  const frontMatter = require("front-matter");
36
37
  if (process.env.NEW_RELIC_ENABLED === "true") {
37
38
  require("newrelic");
@@ -90,18 +91,18 @@ async function fetchComponentsYml() {
90
91
  axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/assessment_components.yml"),
91
92
  axios_1.default.get("https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/docs/explanatory_components.yml"),
92
93
  ]);
93
- const combinedContent = `
94
- # ASSESSMENT COMPONENTS
95
- These components are designed for evaluation and knowledge assessment:
96
-
97
- ${assessmentResponse.data}
98
-
99
- ---
100
-
101
- # EXPLANATORY COMPONENTS
102
- These components are designed for explanation and learning support:
103
-
104
- ${explanatoryResponse.data}
94
+ const combinedContent = `
95
+ # ASSESSMENT COMPONENTS
96
+ These components are designed for evaluation and knowledge assessment:
97
+
98
+ ${assessmentResponse.data}
99
+
100
+ ---
101
+
102
+ # EXPLANATORY COMPONENTS
103
+ These components are designed for explanation and learning support:
104
+
105
+ ${explanatoryResponse.data}
105
106
  `;
106
107
  return combinedContent;
107
108
  }
@@ -135,10 +136,10 @@ const createInitialSidebar = async (slugs, initialLanguage = "en") => {
135
136
  return sidebar;
136
137
  };
137
138
  const uploadInitialReadme = async (bucket, exSlug, targetDir, packageContext) => {
138
- const isGeneratingText = `
139
- \`\`\`loader slug="${exSlug}"
140
- :rigo
141
- \`\`\`
139
+ const isGeneratingText = `
140
+ \`\`\`loader slug="${exSlug}"
141
+ :rigo
142
+ \`\`\`
142
143
  `;
143
144
  const readmeFilename = `README${(0, creatorUtilities_1.getReadmeExtension)(packageContext.language || "en")}`;
144
145
  await uploadFileToBucket(bucket, isGeneratingText, `${targetDir}/${readmeFilename}`);
@@ -148,7 +149,7 @@ const cleanFormState = (formState) => {
148
149
  return rest;
149
150
  };
150
151
  const cleanFormStateForSyllabus = (formState) => {
151
- return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
152
+ return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, contentIndex: formState.contentIndex, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
152
153
  };
153
154
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl) => {
154
155
  const availableLangs = Object.keys(courseJson.title);
@@ -182,6 +183,13 @@ const lessonCleaner = (lesson) => {
182
183
  return Object.assign(Object.assign({}, lesson), { duration: undefined, generated: undefined, status: undefined, translations: undefined, uid: undefined, initialContent: undefined, locked: undefined });
183
184
  };
184
185
  async function startInitialContentGeneration(rigoToken, steps, packageContext, exercise, courseSlug, purposeSlug, lastLesson = "") {
186
+ // Defensive validation
187
+ if (!exercise) {
188
+ throw new errorHandler_1.ValidationError("Exercise is required but was not provided");
189
+ }
190
+ if (!exercise.id || !exercise.title) {
191
+ throw new errorHandler_1.ValidationError(`Exercise is missing required properties: id=${exercise.id}, title=${exercise.title}`);
192
+ }
185
193
  const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
186
194
  console.log("Starting initial content generation for", exSlug);
187
195
  const webhookUrl = `${process.env.HOST}/webhooks/${courseSlug}/initial-content-processor/${exercise.uid}/${rigoToken}`;
@@ -649,9 +657,8 @@ class ServeCommand extends SessionCommand_1.default {
649
657
  return res.status(404).json({ error: "Exercise not found" });
650
658
  }
651
659
  const exerciseDir = `courses/${courseSlug}/exercises/${exercise.slug}`;
652
- for (const fileStr of files) {
660
+ for (const fileObj of files) {
653
661
  try {
654
- const fileObj = JSON.parse(fileStr);
655
662
  console.log(`📄 Processing file: ${fileObj.name}`);
656
663
  // Save the main file with content
657
664
  if (fileObj.name && fileObj.content) {
@@ -696,7 +703,7 @@ class ServeCommand extends SessionCommand_1.default {
696
703
  res.status(500).json({ error: error.message });
697
704
  }
698
705
  });
699
- app.post("/actions/continue-generating/:courseSlug/:lessonUid", async (req, res) => {
706
+ app.post("/actions/continue-generating/:courseSlug/:lessonUid", (0, errorHandler_1.asyncHandler)(async (req, res) => {
700
707
  const { courseSlug, lessonUid } = req.params;
701
708
  const { feedback, mode } = req.body;
702
709
  const rigoToken = req.header("x-rigo-token");
@@ -706,13 +713,23 @@ class ServeCommand extends SessionCommand_1.default {
706
713
  // console.log("FEEDBACK", feedback);
707
714
  // console.log("MODE", mode);
708
715
  if (!rigoToken) {
709
- return res.status(400).json({
710
- error: "Rigo token is required. x-rigo-token header is missing",
711
- });
716
+ throw new errorHandler_1.ValidationError("Rigo token is required. x-rigo-token header is missing");
712
717
  }
713
718
  const syllabus = await getSyllabus(courseSlug, bucket);
714
719
  const exercise = syllabus.lessons.find(lesson => lesson.uid === lessonUid);
715
720
  const exerciseIndex = syllabus.lessons.findIndex(lesson => lesson.uid === lessonUid);
721
+ // Validate that exercise exists
722
+ if (!exercise) {
723
+ throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
724
+ }
725
+ // Validate that exercise index is valid (defensive check)
726
+ if (exerciseIndex === -1) {
727
+ throw new errorHandler_1.NotFoundError(`Lesson with UID "${lessonUid}" not found in course "${courseSlug}"`);
728
+ }
729
+ // Validate that exercise has required properties
730
+ if (!exercise.id || !exercise.title) {
731
+ throw new errorHandler_1.ValidationError(`Lesson "${lessonUid}" is missing required properties (id or title)`);
732
+ }
716
733
  // previous exercise
717
734
  let previousReadme = "---";
718
735
  const previousExercise = syllabus.lessons[exerciseIndex - 1];
@@ -753,7 +770,7 @@ class ServeCommand extends SessionCommand_1.default {
753
770
  syllabus.generationMode = mode;
754
771
  await saveSyllabus(courseSlug, syllabus, bucket);
755
772
  res.json({ status: "SUCCESS" });
756
- });
773
+ }));
757
774
  // TODO: Check if this command is being used
758
775
  app.post("/actions/generate-image/:courseSlug", async (req, res) => {
759
776
  const rigoToken = req.header("x-rigo-token");
@@ -1383,22 +1400,49 @@ class ServeCommand extends SessionCommand_1.default {
1383
1400
  res.set("Content-Disposition", `attachment; filename="${file}"`);
1384
1401
  fileStream.pipe(res);
1385
1402
  });
1386
- app.put("/exercise/:slug/file/:fileName", express.text(), async (req, res) => {
1403
+ app.put("/exercise/:slug/file/:fileName", express.text(), (0, errorHandler_1.asyncHandler)(async (req, res) => {
1404
+ var _a, _b;
1387
1405
  const { slug, fileName } = req.params;
1388
1406
  const query = req.query;
1389
- console.log(`PUT /exercise/${slug}/file/${fileName}`);
1390
1407
  const courseSlug = query.slug;
1391
- // console.log("COURSE SLUG", courseSlug)
1392
- // Update the file in the bucket
1393
- const file = await bucket.file(`courses/${courseSlug}/exercises/${slug}/${fileName}`);
1394
- await file.save(req.body);
1395
- const created = await file.exists();
1396
- // console.log("File updated", created)
1397
- res.send({
1398
- message: "File updated",
1399
- created,
1400
- });
1401
- });
1408
+ console.log(`PUT /exercise/${slug}/file/${fileName}`);
1409
+ // Validate required parameters
1410
+ if (!courseSlug) {
1411
+ throw new errorHandler_1.ValidationError("Course slug is required in query parameters");
1412
+ }
1413
+ if (!fileName || !slug) {
1414
+ throw new errorHandler_1.ValidationError("File name and exercise slug are required");
1415
+ }
1416
+ try {
1417
+ // Update the file in the bucket
1418
+ const file = bucket.file(`courses/${courseSlug}/exercises/${slug}/${fileName}`);
1419
+ await file.save(req.body, {
1420
+ resumable: false,
1421
+ });
1422
+ const created = await file.exists();
1423
+ res.send({
1424
+ message: "File updated",
1425
+ created,
1426
+ });
1427
+ }
1428
+ catch (error) {
1429
+ // Handle Google Cloud Storage rate limit errors (429)
1430
+ if (error.code === 429 ||
1431
+ ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("rate limit")) ||
1432
+ ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("rateLimitExceeded"))) {
1433
+ throw new errorHandler_1.ConflictError("Storage rate limit exceeded. Please try again in a few moments.", {
1434
+ code: "STORAGE_RATE_LIMIT",
1435
+ retryAfter: 60, // seconds
1436
+ });
1437
+ }
1438
+ // Handle other GCS errors
1439
+ if (error.code) {
1440
+ throw new errorHandler_1.InternalServerError(`Storage error: ${error.message || "Failed to update file"}`, { code: error.code });
1441
+ }
1442
+ // Re-throw if it's already an operational error
1443
+ throw error;
1444
+ }
1445
+ }));
1402
1446
  // Create a new step for a course
1403
1447
  app.post("/course/:slug/create-step", async (req, res) => {
1404
1448
  console.log("POST /course/:slug/create-step");
@@ -2393,6 +2437,10 @@ class ServeCommand extends SessionCommand_1.default {
2393
2437
  .json({ error: "Export failed", details: error.message });
2394
2438
  }
2395
2439
  });
2440
+ // 404 error handler
2441
+ app.use(errorHandler_1.notFoundHandler);
2442
+ // Global error handler
2443
+ app.use(errorHandler_1.errorHandler);
2396
2444
  server.listen(PORT, () => {
2397
2445
  console.log(`🚀 Creator UI server running at http://localhost:${PORT}/creator`);
2398
2446
  });