@learnpack/learnpack 5.0.358 → 5.0.359

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.
@@ -1,14 +1,4 @@
1
1
  import SessionCommand from "../utils/SessionCommand";
2
- type ExistingAssetInfo = {
3
- lang: string;
4
- slug: string;
5
- exists: boolean;
6
- academyId?: number;
7
- };
8
- export declare const handleAssetCreation: (sessionPayload: {
9
- token: string;
10
- rigobotToken: string;
11
- }, learnJson: any, selectedLang: string, learnpackDeployUrl: string, b64IndexReadme: string, academyId: number | undefined, learnpackId: number, preflightInfo?: ExistingAssetInfo, all_translations?: string[]) => Promise<any>;
12
2
  declare class BuildCommand extends SessionCommand {
13
3
  static description: string;
14
4
  static flags: {
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAssetCreation = void 0;
4
3
  /* eslint-disable arrow-parens */
5
4
  /* eslint-disable unicorn/no-array-for-each */
6
5
  const child_process_1 = require("child_process");
@@ -20,181 +19,55 @@ const rigoActions_1 = require("../utils/rigoActions");
20
19
  const misc_1 = require("../utils/misc");
21
20
  const creatorUtilities_1 = require("../utils/creatorUtilities");
22
21
  const branding_1 = require("../utils/branding");
22
+ const assetResolver_1 = require("../utils/assetResolver");
23
+ const assetSync_1 = require("../utils/assetSync");
23
24
  const uploadZipEndpont = api_1.RIGOBOT_HOST + "/v1/learnpack/upload";
24
- const getAvailableLangs = (learnJson) => {
25
- const langs = Object.keys((learnJson === null || learnJson === void 0 ? void 0 : learnJson.title) || {});
26
- return langs.filter((l) => typeof l === "string" && l.length > 0);
27
- };
28
25
  const getDefaultLang = (learnJson) => {
29
- const availableLangs = getAvailableLangs(learnJson);
26
+ const availableLangs = (0, assetResolver_1.getAvailableLangs)(learnJson);
30
27
  if (availableLangs.length === 0)
31
28
  return "en";
32
29
  return availableLangs.includes("en") ? "en" : availableLangs[0];
33
30
  };
34
- const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) => {
35
- if (!translations || typeof translations !== "object")
36
- return "";
37
- const direct = translations[lang];
38
- if (typeof direct === "string" && direct.trim().length > 0)
39
- return direct;
40
- for (const fb of fallbackLangs) {
41
- const v = translations[fb];
42
- if (typeof v === "string" && v.trim().length > 0)
43
- return v;
44
- }
45
- const firstKey = Object.keys(translations)[0];
46
- const first = firstKey ? translations[firstKey] : "";
47
- return typeof first === "string" ? first : "";
48
- };
49
- const getExistingAssetsInfo = async (token, learnJson) => {
50
- const availableLangs = getAvailableLangs(learnJson);
51
- const results = [];
52
- for (const lang of availableLangs) {
53
- const assetTitle = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, lang);
54
- if (!assetTitle)
55
- continue;
56
- let slug = (0, creatorUtilities_1.slugify)(assetTitle).slice(0, 47);
57
- slug = `${slug}-${lang}`;
58
- // eslint-disable-next-line no-await-in-loop
59
- const { exists, academyId } = await api_1.default.doesAssetExists(token, slug);
60
- results.push({ lang, slug, exists, academyId });
61
- }
62
- return results;
63
- };
64
- const determinePublishAcademyMode = (existingAssets) => {
65
- const academyIds = existingAssets
66
- .filter((a) => a.exists && a.academyId !== undefined)
67
- .map((a) => a.academyId);
68
- const unique = [...new Set(academyIds)];
69
- if (unique.length === 0)
70
- return { type: "select" };
71
- if (unique.length === 1)
72
- return { type: "locked", academyId: unique[0] };
73
- return { type: "conflict", academies: unique };
74
- };
75
- const handleAssetCreation = async (sessionPayload, learnJson, selectedLang, learnpackDeployUrl, b64IndexReadme, academyId, learnpackId, preflightInfo, all_translations = []) => {
76
- const category = "uncategorized";
31
+ const readReadmeFromDisk = async (lang) => {
32
+ const readmePath = path.join(process.cwd(), `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
77
33
  try {
78
- const user = await api_1.default.validateToken(sessionPayload.token);
79
- const assetTitle = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, selectedLang);
80
- const assetDescription = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.description, selectedLang);
81
- if (!assetTitle) {
82
- throw new Error(`Missing learn.json title for language "${selectedLang}"`);
83
- }
84
- let slug = (0, creatorUtilities_1.slugify)(assetTitle).slice(0, 47);
85
- slug = `${slug}-${selectedLang}`;
86
- // Use pre-flight info when available to avoid an extra GET request
87
- const { exists, academyId: existingAcademyId } = preflightInfo !== null && preflightInfo !== void 0 ? preflightInfo : (await api_1.default.doesAssetExists(sessionPayload.token, slug));
88
- // const technologies: unknown[] = Array.isArray(learnJson?.technologies) ?
89
- // learnJson.technologies :
90
- // []
91
- if (!exists) {
92
- console_1.default.info("Asset does not exist in this academy, creating it");
93
- const assetPayload = {
94
- slug: slug,
95
- title: assetTitle,
96
- lang: selectedLang,
97
- graded: true,
98
- description: assetDescription,
99
- learnpack_deploy_url: learnpackDeployUrl,
100
- // technologies: technologies.map((tech: unknown) =>
101
- // String(tech).toLowerCase().replace(/\s+/g, "-")
102
- // ),
103
- technologies: [],
104
- url: learnpackDeployUrl,
105
- category: category,
106
- owner: user.id,
107
- author: user.id,
108
- preview: learnJson.preview,
109
- readme_raw: b64IndexReadme,
110
- all_translations,
111
- learnpack_id: learnpackId,
112
- };
113
- if (academyId !== undefined) {
114
- assetPayload.academy_id = academyId;
115
- }
116
- const asset = await api_1.default.createAsset(sessionPayload.token, assetPayload);
117
- try {
118
- await api_1.default.updateRigoPackage(sessionPayload.rigobotToken.trim(), learnJson.slug, {
119
- asset_id: asset.id,
120
- });
121
- }
122
- catch (error) {
123
- console_1.default.error("Error updating Rigo package:", error);
124
- }
125
- console_1.default.info("Asset created with id", asset.id);
126
- return asset;
127
- }
128
- console_1.default.info("Asset exists, updating it");
129
- const updatePayload = {
130
- graded: true,
131
- learnpack_deploy_url: learnpackDeployUrl,
132
- title: assetTitle,
133
- category: category,
134
- description: assetDescription,
135
- all_translations,
136
- learnpack_id: learnpackId,
137
- };
138
- // Only set academy when the asset has none yet and the user selected one
139
- if (existingAcademyId === undefined && academyId !== undefined) {
140
- updatePayload.academy_id = academyId;
141
- }
142
- const asset = await api_1.default.updateAsset(sessionPayload.token, slug, updatePayload);
143
- try {
144
- await api_1.default.updateRigoPackage(sessionPayload.rigobotToken.trim(), learnJson.slug, {
145
- asset_id: asset.id,
146
- });
147
- }
148
- catch (error) {
149
- console_1.default.error("Error updating Rigo package:", error);
150
- }
151
- console_1.default.info("Asset updated with id", asset.id);
152
- return asset;
34
+ return fs.existsSync(readmePath) ?
35
+ fs.readFileSync(readmePath, "utf-8") :
36
+ "";
153
37
  }
154
38
  catch (error) {
155
- console_1.default.error("Error updating or creating asset:", error);
156
- throw error;
39
+ console_1.default.error("Error reading index readme:", error);
40
+ return "";
157
41
  }
158
42
  };
159
- exports.handleAssetCreation = handleAssetCreation;
160
- const createMultiLangAssetFromDisk = async (sessionPayload, learnJson, deployUrl, selectedAcademyId, existingAssetsInfo = []) => {
161
- const availableLangs = getAvailableLangs(learnJson);
162
- if (availableLangs.length === 0) {
43
+ const syncAssetsFromDisk = async (sessionPayload, learnJson, deployUrl, selectedAcademyId, resolution) => {
44
+ if ((0, assetResolver_1.getAvailableLangs)(learnJson).length === 0) {
163
45
  console_1.default.error("No languages found in learn.json.title. Add at least one language (e.g. title.en).");
164
46
  return;
165
47
  }
166
- const learnpackId = await api_1.default.resolveLearnpackPackageId(sessionPayload.rigobotToken, learnJson.slug);
167
- if (learnpackId === null) {
168
- console_1.default.warning("Breathecode assets skipped: could not resolve Learnpack package id");
169
- return;
48
+ // The pre-upload resolution is reused unless the package did not exist in
49
+ // Rigobot yet (first publish): the upload has just created it.
50
+ const result = await (0, assetSync_1.syncPackageAssets)({
51
+ bcToken: sessionPayload.token,
52
+ rigoToken: sessionPayload.rigobotToken,
53
+ packageSlug: learnJson.slug,
54
+ learnJson,
55
+ deployUrl,
56
+ academyId: selectedAcademyId,
57
+ readReadme: readReadmeFromDisk,
58
+ resolution: resolution.learnpackId === null ? undefined : resolution,
59
+ });
60
+ for (const warning of result.warnings)
61
+ console_1.default.warning(warning);
62
+ for (const synced of result.synced) {
63
+ console_1.default.info(`Asset ${synced.action} for "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`);
170
64
  }
171
- const all_translations = [];
172
- for (const lang of availableLangs) {
173
- const readmePath = path.join(process.cwd(), `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
174
- let indexReadmeString = "";
175
- try {
176
- if (fs.existsSync(readmePath)) {
177
- indexReadmeString = fs.readFileSync(readmePath, "utf-8");
178
- }
65
+ for (const error of result.errors) {
66
+ if (error.kind === "lang_error") {
67
+ console_1.default.error(`Asset for "${error.lang}": ${error.error.detail}`);
179
68
  }
180
- catch (error) {
181
- console_1.default.error("Error reading index readme:", error);
182
- indexReadmeString = "";
183
- }
184
- const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64");
185
- const preflightInfo = existingAssetsInfo.find((a) => a.lang === lang);
186
- try {
187
- // eslint-disable-next-line no-await-in-loop
188
- const asset = await (0, exports.handleAssetCreation)(sessionPayload, learnJson, lang, deployUrl, b64IndexReadme, selectedAcademyId, learnpackId, preflightInfo, all_translations);
189
- if (!asset) {
190
- console_1.default.debug("Could not create/update asset for lang", lang);
191
- continue;
192
- }
193
- all_translations.push(asset.slug);
194
- }
195
- catch (error) {
196
- console_1.default.error("Error creating asset for language", lang, error);
197
- // Continue with other languages
69
+ else {
70
+ console_1.default.error(error.error.detail);
198
71
  }
199
72
  }
200
73
  };
@@ -313,8 +186,19 @@ class BuildCommand extends SessionCommand_1.default {
313
186
  process.exit(1);
314
187
  }
315
188
  console_1.default.info("Checking existing assets...");
316
- const existingAssetsInfo = await getExistingAssetsInfo(sessionPayload.token, learnJson);
317
- const academyMode = determinePublishAcademyMode(existingAssetsInfo);
189
+ const resolution = await (0, assetResolver_1.resolvePackageAssets)({
190
+ bcToken: sessionPayload.token,
191
+ rigoToken: rigoToken,
192
+ packageSlug: learnJson.slug,
193
+ learnJson,
194
+ });
195
+ for (const warning of resolution.warnings)
196
+ console_1.default.warning(warning);
197
+ if (resolution.packageState === "unknown") {
198
+ console_1.default.error(`${resolution.packageError}. Existing assets cannot be checked, so publishing now could create duplicates. Try again later.`);
199
+ process.exit(1);
200
+ }
201
+ const academyMode = (0, assetResolver_1.determineAcademyMode)(resolution);
318
202
  let selectedAcademyId;
319
203
  if (academyMode.type === "conflict") {
320
204
  console_1.default.warning(`Some of your assets are associated with different academies ` +
@@ -380,9 +264,9 @@ class BuildCommand extends SessionCommand_1.default {
380
264
  if (fs.existsSync(indexHtmlPath)) {
381
265
  let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf-8");
382
266
  const selectedLang = getDefaultLang(learnJson);
383
- const description = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.description, selectedLang) ||
267
+ const description = (0, assetSync_1.getLocalizedValue)(learnJson === null || learnJson === void 0 ? void 0 : learnJson.description, selectedLang) ||
384
268
  "LearnPack is awesome!";
385
- const title = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, selectedLang) ||
269
+ const title = (0, assetSync_1.getLocalizedValue)(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, selectedLang) ||
386
270
  "LearnPack: Interactive Learning as a Service";
387
271
  const previewUrl = learnJson.preview || (0, branding_1.getFallbackPreviewUrl)();
388
272
  // Replace placeholders and the <title>Old title </title> tag for a new tag with the title
@@ -402,9 +286,9 @@ class BuildCommand extends SessionCommand_1.default {
402
286
  if (fs.existsSync(manifestPWA)) {
403
287
  let manifestPWAContent = fs.readFileSync(manifestPWA, "utf-8");
404
288
  const selectedLang = getDefaultLang(learnJson);
405
- const courseTitle = getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, selectedLang) ||
406
- getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, "us") ||
407
- getLocalizedValue(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, "en") ||
289
+ const courseTitle = (0, assetSync_1.getLocalizedValue)(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, selectedLang) ||
290
+ (0, assetSync_1.getLocalizedValue)(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, "us") ||
291
+ (0, assetSync_1.getLocalizedValue)(learnJson === null || learnJson === void 0 ? void 0 : learnJson.title, "en") ||
408
292
  "LearnPack";
409
293
  manifestPWAContent = manifestPWAContent.replace("{{course_title}}", courseTitle);
410
294
  const courseShortName = { answer: "testing-tutorial" };
@@ -461,7 +345,7 @@ class BuildCommand extends SessionCommand_1.default {
461
345
  console.log(res.data);
462
346
  fs.unlinkSync(zipFilePath);
463
347
  this.removeDirectory(buildDir);
464
- await createMultiLangAssetFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url, selectedAcademyId, existingAssetsInfo);
348
+ await syncAssetsFromDisk({ token: sessionPayload.token, rigobotToken: rigoToken }, learnJson, res.data.url, selectedAcademyId, resolution);
465
349
  }
466
350
  catch (error) {
467
351
  if (axios_1.default.isAxiosError(error)) {
@@ -31,7 +31,8 @@ const misc_1 = require("../utils/misc");
31
31
  const configBuilder_1 = require("../utils/configBuilder");
32
32
  const creatorUtilities_2 = require("../utils/creatorUtilities");
33
33
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
34
- const publish_1 = require("./publish");
34
+ const assetResolver_1 = require("../utils/assetResolver");
35
+ const assetSync_1 = require("../utils/assetSync");
35
36
  const export_1 = require("../utils/export");
36
37
  const router_1 = require("../services/ingest/http/router");
37
38
  const bucketIo_1 = require("../utils/coursePackage/bucketIo");
@@ -319,94 +320,34 @@ const cleanFormState = (formState) => {
319
320
  const cleanFormStateForSyllabus = (formState) => {
320
321
  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 });
321
322
  };
322
- const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) => {
323
- if (!translations || typeof translations !== "object")
324
- return "";
325
- const direct = translations[lang];
326
- if (typeof direct === "string" && direct.trim().length > 0)
327
- return direct;
328
- for (const fb of fallbackLangs) {
329
- const v = translations[fb];
330
- if (typeof v === "string" && v.trim().length > 0)
331
- return v;
332
- }
333
- const firstKey = Object.keys(translations)[0];
334
- const first = firstKey ? translations[firstKey] : "";
335
- return typeof first === "string" ? first : "";
336
- };
337
- function assetSyncErrorDetail(err) {
338
- if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
339
- return err.detail;
340
- if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
341
- return err.message;
342
- try {
343
- return JSON.stringify(err);
344
- }
345
- catch (_a) {
346
- return String(err);
347
- }
348
- }
349
323
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl, academyId) => {
350
- var _a;
351
- const learnpackId = await (0, api_1.resolveLearnpackPackageId)(rigoToken, courseSlug);
352
- if (learnpackId === null) {
353
- return {
354
- errors: [
355
- {
356
- kind: "package_error",
357
- error: {
358
- detail: "Could not resolve Learnpack package id; assets not synced to Breathecode.",
359
- },
360
- },
361
- ],
362
- };
363
- }
364
- const availableLangs = Object.keys(courseJson.title);
365
- console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
366
- const all_translations = [];
367
- const errors = [];
368
- for (const lang of availableLangs) {
369
- // eslint-disable-next-line no-await-in-loop
370
- const indexReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
371
- let indexReadmeString = "";
324
+ const readReadme = async (lang) => {
325
+ const indexReadme = bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
372
326
  try {
373
- // eslint-disable-next-line no-await-in-loop
374
- const [indexReadmeContent] = await indexReadme.download();
375
- indexReadmeString = indexReadmeContent.toString();
327
+ const [content] = await indexReadme.download();
328
+ return content.toString();
376
329
  }
377
330
  catch (error) {
378
331
  console.error("Error downloading index readme", error);
379
332
  // TODO: Trigger generation of the index readme
380
- indexReadmeString = "";
381
- }
382
- const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
383
- try {
384
- // eslint-disable-next-line no-await-in-loop
385
- const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, learnpackId, undefined, all_translations);
386
- if (!asset) {
387
- errors.push({
388
- kind: "lang_error",
389
- lang,
390
- error: { detail: "Failed to create asset" },
391
- });
392
- console.log("No se pudo crear el asset, saltando idioma", lang);
393
- continue;
394
- }
395
- all_translations.push(asset.slug);
396
- }
397
- catch (error) {
398
- const errorData = error && typeof error === "object" && "response" in error ?
399
- ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error :
400
- error;
401
- errors.push({
402
- kind: "lang_error",
403
- lang,
404
- error: { detail: assetSyncErrorDetail(errorData) },
405
- });
406
- console.error(`Error creating asset for language ${lang}:`, error);
333
+ return "";
407
334
  }
335
+ };
336
+ const result = await (0, assetSync_1.syncPackageAssets)({
337
+ bcToken,
338
+ rigoToken,
339
+ packageSlug: courseSlug,
340
+ learnJson: courseJson,
341
+ deployUrl,
342
+ academyId,
343
+ readReadme,
344
+ });
345
+ for (const warning of result.warnings)
346
+ console.warn("[assets]", warning);
347
+ for (const synced of result.synced) {
348
+ console.log(`[assets] ${synced.action} "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`);
408
349
  }
409
- return { errors };
350
+ return { errors: result.errors };
410
351
  };
411
352
  const lessonCleaner = (lesson) => {
412
353
  return Object.assign(Object.assign({}, lesson), { duration: undefined, generated: undefined, status: undefined, translations: undefined, uid: undefined, initialContent: undefined, locked: undefined });
@@ -3713,52 +3654,47 @@ class ServeCommand extends SessionCommand_1.default {
3713
3654
  error: "Authentication failed, missing breathecode token",
3714
3655
  });
3715
3656
  }
3657
+ // Older IDEs only send the Breathecode token; without the Rigobot
3658
+ // token the lookup falls back to the title-derived slugs.
3659
+ const rigoToken = req.header("x-rigo-token");
3716
3660
  const configFile = await bucket.file(`courses/${slug}/.learn/config.json`);
3717
3661
  const [configContent] = await configFile.download();
3718
3662
  const configJson = JSON.parse(configContent.toString());
3719
3663
  const { config } = configJson;
3720
- const availableLangs = Object.keys(config.title || {});
3721
- let isPublished = false;
3722
- // Collect academy ids from all existing assets
3723
- const foundAcademyIds = [];
3724
- const slugsToCheck = availableLangs
3725
- .map(lang => {
3726
- const assetTitle = getLocalizedValue(config.title, lang);
3727
- if (!assetTitle)
3728
- return null;
3729
- const assetSlug = (0, creatorUtilities_2.slugify)(assetTitle).slice(0, 47);
3730
- return `${assetSlug}-${lang}`;
3731
- })
3732
- .filter((slug) => slug !== null);
3733
- const assetChecks = await Promise.all(slugsToCheck.map(slug => (0, api_1.doesAssetExists)(bcToken, slug)));
3734
- for (const { exists, academyId: existingAcademyId } of assetChecks) {
3735
- if (exists) {
3736
- isPublished = true;
3737
- if (existingAcademyId !== undefined) {
3738
- foundAcademyIds.push(existingAcademyId);
3739
- }
3664
+ const resolution = await (0, assetResolver_1.resolvePackageAssets)({
3665
+ bcToken,
3666
+ rigoToken,
3667
+ packageSlug: slug,
3668
+ learnJson: config,
3669
+ });
3670
+ for (const warning of resolution.warnings) {
3671
+ console.warn("[package-academy]", warning);
3672
+ }
3673
+ const isPublished = (0, assetResolver_1.hasPublishedAsset)(resolution);
3674
+ const academyMode = (0, assetResolver_1.determineAcademyMode)(resolution);
3675
+ const warnings = [...resolution.warnings];
3676
+ if (resolution.packageState === "unknown") {
3677
+ warnings.push(`${resolution.packageError}; existing assets could not be checked.`);
3678
+ }
3679
+ for (const [lang, entry] of resolution.byLang) {
3680
+ if (entry.status === "unknown") {
3681
+ warnings.push(`"${lang}": ${entry.error}`);
3682
+ }
3683
+ else if (entry.status === "ambiguous") {
3684
+ warnings.push(`"${lang}": ${entry.candidates.length} assets found (${entry.candidates
3685
+ .map(c => `#${c.id} ${c.slug}`)
3686
+ .join(", ")})`);
3740
3687
  }
3741
3688
  }
3742
- const uniqueAcademies = [...new Set(foundAcademyIds)];
3743
- let mode;
3744
- let lockedAcademyId;
3745
- let conflictAcademies;
3746
- if (uniqueAcademies.length === 0) {
3747
- mode = "select";
3748
- }
3749
- else if (uniqueAcademies.length === 1) {
3750
- mode = "locked";
3751
- lockedAcademyId = uniqueAcademies[0];
3752
- }
3753
- else {
3754
- mode = "conflict";
3755
- conflictAcademies = uniqueAcademies;
3756
- }
3689
+ const mode = academyMode.type;
3690
+ const lockedAcademyId = academyMode.type === "locked" ? academyMode.academyId : undefined;
3691
+ const conflictAcademies = academyMode.type === "conflict" ? academyMode.academies : undefined;
3757
3692
  return res.json({
3758
3693
  isPublished,
3759
3694
  mode,
3760
3695
  lockedAcademyId,
3761
3696
  conflictAcademies,
3697
+ warnings,
3762
3698
  });
3763
3699
  }
3764
3700
  catch (error) {
@@ -1,3 +1,4 @@
1
+ export declare const BREATHECODE_HOST = "https://breathecode.herokuapp.com";
1
2
  /**
2
3
  * Student telemetry, and publication events. Both share the endpoint: a body
3
4
  * carrying a top-level `event` key is dispatched as an event, anything else
@@ -31,13 +32,30 @@ export type AssetSyncError = {
31
32
  detail: string;
32
33
  };
33
34
  };
35
+ export type RigoPackageLookup = {
36
+ kind: "found";
37
+ id: number;
38
+ assetIds: number[];
39
+ } | {
40
+ kind: "missing";
41
+ } | {
42
+ kind: "error";
43
+ error: string;
44
+ };
34
45
  /**
35
- * GET Rigobot package by slug after a successful upload. Does not throw.
36
- * @param rigoToken Rigobot API token (Bearer-style `Token` header value).
46
+ * Human-readable summary of a failed axios call, for warnings and error payloads.
47
+ * @param error - Whatever the request rejected with.
48
+ * @returns "HTTP <status>" when the server answered, the error message otherwise.
49
+ */
50
+ export declare function describeRequestError(error: unknown): string;
51
+ /**
52
+ * GET a Rigobot package by slug. Distinguishes "no such package" from
53
+ * "could not check", so callers can fail closed on the latter.
54
+ * @param rigoToken Rigobot API token (`Token` header value).
37
55
  * @param courseSlug Learnpack package slug used in the Rigobot URL path.
38
- * @returns Resolved numeric package id from Rigobot, or `null` if the request fails, the id is missing, or it is not a finite integer.
56
+ * @returns The package id and its asset_ids, `missing` on 404, `error` otherwise (403 included: a package owned by somebody else is not ours to read).
39
57
  */
40
- export declare function resolveLearnpackPackageId(rigoToken: string, courseSlug: string): Promise<number | null>;
58
+ export declare function getRigoPackage(rigoToken: string, courseSlug: string): Promise<RigoPackageLookup>;
41
59
  type TAssetMissing = {
42
60
  slug: string;
43
61
  title: string;
@@ -57,10 +75,41 @@ type TAssetMissing = {
57
75
  learnpack_id: number;
58
76
  };
59
77
  export declare const createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
60
- export declare const doesAssetExists: (token: string, assetSlug: string) => Promise<{
61
- exists: boolean;
62
- academyId?: number;
63
- }>;
78
+ /** The subset of a Breathecode asset the publish flow reads. */
79
+ export type BcAsset = {
80
+ id: number;
81
+ slug: string;
82
+ lang: string | null;
83
+ status: string | null;
84
+ learnpackId: number | null;
85
+ learnpackDeployUrl: string | null;
86
+ ownerId: number | null;
87
+ academyId: number | null;
88
+ };
89
+ export type AssetLookup = {
90
+ kind: "found";
91
+ asset: BcAsset;
92
+ } | {
93
+ kind: "missing";
94
+ } | {
95
+ kind: "error";
96
+ error: string;
97
+ };
98
+ /**
99
+ * GET a Breathecode asset by numeric id. The endpoint applies no status or
100
+ * visibility filter for a single asset, so this is the stable anchor.
101
+ * @param token Breathecode token.
102
+ * @param assetId Asset primary key.
103
+ * @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
104
+ */
105
+ export declare const getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
106
+ /**
107
+ * GET a Breathecode asset by slug.
108
+ * @param token Breathecode token.
109
+ * @param assetSlug Asset slug.
110
+ * @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
111
+ */
112
+ export declare const getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
64
113
  type TTechnology = {
65
114
  slug: string;
66
115
  lang: string;
@@ -83,18 +132,16 @@ declare const _default: {
83
132
  listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
84
133
  validateToken: (token: string) => Promise<any>;
85
134
  createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
86
- doesAssetExists: (token: string, assetSlug: string) => Promise<{
87
- exists: boolean;
88
- academyId?: number;
89
- }>;
90
- updateAsset: (token: string, assetSlug: string, asset: Partial<TAssetMissing> & {
135
+ getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
136
+ getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
137
+ updateAsset: (token: string, assetId: number, asset: Partial<TAssetMissing> & {
91
138
  learnpack_id: number;
92
139
  }) => Promise<any>;
93
- resolveLearnpackPackageId: typeof resolveLearnpackPackageId;
140
+ getRigoPackage: typeof getRigoPackage;
94
141
  getCategories: (token: string) => Promise<any>;
95
142
  updateRigoPackage: (token: string, slug: string, updates: {
96
143
  asset_id?: number;
97
- new_slug?: string;
144
+ asset_ids?: number[];
98
145
  }) => Promise<any>;
99
146
  createRigoPackage: (token: string, slug: string, config: any) => Promise<any>;
100
147
  getCurrentTechnologies: () => TTechnology[];