@learnpack/learnpack 5.0.355 → 5.0.357
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.
- package/lib/commands/serve.d.ts +0 -17
- package/lib/commands/serve.js +159 -129
- package/lib/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
- package/lib/creatorDist/assets/index-zrPponAn.css +1701 -0
- package/lib/creatorDist/index.html +2 -2
- package/lib/models/creator.d.ts +1 -1
- package/lib/services/ingest/adapters/gcsCourseStorage.d.ts +13 -0
- package/lib/services/ingest/adapters/gcsCourseStorage.js +58 -0
- package/lib/services/ingest/adapters/rigoUserTokenAuth.d.ts +15 -0
- package/lib/services/ingest/adapters/rigoUserTokenAuth.js +46 -0
- package/lib/services/ingest/adapters/rigobotPackageRegistry.d.ts +7 -0
- package/lib/services/ingest/adapters/rigobotPackageRegistry.js +119 -0
- package/lib/services/ingest/core/buildIngestConfig.d.ts +27 -0
- package/lib/services/ingest/core/buildIngestConfig.js +40 -0
- package/lib/services/ingest/core/buildInitialSyllabus.d.ts +24 -0
- package/lib/services/ingest/core/buildInitialSyllabus.js +104 -0
- package/lib/services/ingest/core/buildSidebar.d.ts +13 -0
- package/lib/services/ingest/core/buildSidebar.js +28 -0
- package/lib/services/ingest/core/coursePaths.d.ts +31 -0
- package/lib/services/ingest/core/coursePaths.js +50 -0
- package/lib/services/ingest/core/ingestCoursePackage.d.ts +52 -0
- package/lib/services/ingest/core/ingestCoursePackage.js +217 -0
- package/lib/services/ingest/core/normalizeLearnJson.d.ts +25 -0
- package/lib/services/ingest/core/normalizeLearnJson.js +29 -0
- package/lib/services/ingest/core/pairLessons.d.ts +29 -0
- package/lib/services/ingest/core/pairLessons.js +102 -0
- package/lib/services/ingest/core/planWrites.d.ts +44 -0
- package/lib/services/ingest/core/planWrites.js +59 -0
- package/lib/services/ingest/core/resolveSlug.d.ts +53 -0
- package/lib/services/ingest/core/resolveSlug.js +93 -0
- package/lib/services/ingest/core/types.d.ts +53 -0
- package/lib/services/ingest/core/types.js +2 -0
- package/lib/services/ingest/core/validatePackage.d.ts +38 -0
- package/lib/services/ingest/core/validatePackage.js +97 -0
- package/lib/services/ingest/createIngestService.d.ts +34 -0
- package/lib/services/ingest/createIngestService.js +38 -0
- package/lib/services/ingest/errors.d.ts +22 -0
- package/lib/services/ingest/errors.js +34 -0
- package/lib/services/ingest/http/errorMapping.d.ts +8 -0
- package/lib/services/ingest/http/errorMapping.js +40 -0
- package/lib/services/ingest/http/router.d.ts +32 -0
- package/lib/services/ingest/http/router.js +124 -0
- package/lib/services/ingest/http/zipPackageReader.d.ts +26 -0
- package/lib/services/ingest/http/zipPackageReader.js +62 -0
- package/lib/services/ingest/ports/courseStorage.d.ts +25 -0
- package/lib/services/ingest/ports/courseStorage.js +2 -0
- package/lib/services/ingest/ports/packageRegistry.d.ts +58 -0
- package/lib/services/ingest/ports/packageRegistry.js +2 -0
- package/lib/services/ingest/ports/requestAuthenticator.d.ts +25 -0
- package/lib/services/ingest/ports/requestAuthenticator.js +2 -0
- package/lib/utils/coursePackage/bucketIo.d.ts +23 -0
- package/lib/utils/coursePackage/bucketIo.js +36 -0
- package/lib/utils/coursePackage/learnJson.d.ts +26 -0
- package/lib/utils/coursePackage/learnJson.js +36 -0
- package/lib/utils/coursePackage/sidebar.d.ts +15 -0
- package/lib/utils/coursePackage/sidebar.js +26 -0
- package/lib/utils/creatorSocket.js +15 -21
- package/lib/utils/gcpCredentials.d.ts +18 -0
- package/lib/utils/gcpCredentials.js +27 -0
- package/lib/utils/rigoActions.d.ts +16 -0
- package/lib/utils/rigoActions.js +102 -1
- package/lib/utils/socketRegistry.d.ts +46 -0
- package/lib/utils/socketRegistry.js +83 -0
- package/package.json +1 -1
- package/src/commands/serve.ts +162 -114
- package/src/creator/README.md +63 -51
- package/src/creator/package-lock.json +1188 -8
- package/src/creator/package.json +5 -1
- package/src/creator/src/App.tsx +120 -54
- package/src/creator/src/components/FileUploader.tsx +1 -12
- package/src/creator/src/components/NotificationListener.tsx +1 -8
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +101 -36
- package/src/creator/src/locales/en.json +8 -0
- package/src/creator/src/locales/es.json +8 -0
- package/src/creator/src/utils/completionResult.test.ts +144 -0
- package/src/creator/src/utils/completionResult.ts +154 -0
- package/src/creator/src/utils/constants.ts +17 -4
- package/src/creator/src/utils/socket.test.ts +113 -0
- package/src/creator/src/utils/socket.ts +60 -37
- package/src/creator/src/utils/store.ts +84 -67
- package/src/creator/src/utils/useCompletionWatchdog.test.ts +99 -0
- package/src/creator/src/utils/useCompletionWatchdog.ts +37 -0
- package/src/creator/vitest.config.ts +16 -0
- package/src/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
- package/src/creatorDist/assets/index-zrPponAn.css +1701 -0
- package/src/creatorDist/index.html +2 -2
- package/src/models/creator.ts +1 -1
- package/src/services/ingest/adapters/gcsCourseStorage.ts +70 -0
- package/src/services/ingest/adapters/rigoUserTokenAuth.ts +65 -0
- package/src/services/ingest/adapters/rigobotPackageRegistry.ts +161 -0
- package/src/services/ingest/core/buildIngestConfig.ts +51 -0
- package/src/services/ingest/core/buildInitialSyllabus.ts +122 -0
- package/src/services/ingest/core/buildSidebar.ts +32 -0
- package/src/services/ingest/core/coursePaths.ts +59 -0
- package/src/services/ingest/core/ingestCoursePackage.ts +343 -0
- package/src/services/ingest/core/normalizeLearnJson.ts +47 -0
- package/src/services/ingest/core/pairLessons.ts +132 -0
- package/src/services/ingest/core/planWrites.ts +91 -0
- package/src/services/ingest/core/resolveSlug.ts +119 -0
- package/src/services/ingest/core/types.ts +57 -0
- package/src/services/ingest/core/validatePackage.ts +132 -0
- package/src/services/ingest/createIngestService.ts +66 -0
- package/src/services/ingest/errors.ts +45 -0
- package/src/services/ingest/http/errorMapping.ts +49 -0
- package/src/services/ingest/http/router.ts +156 -0
- package/src/services/ingest/http/zipPackageReader.ts +81 -0
- package/src/services/ingest/ports/courseStorage.ts +25 -0
- package/src/services/ingest/ports/packageRegistry.ts +60 -0
- package/src/services/ingest/ports/requestAuthenticator.ts +28 -0
- package/src/ui/_app/app.js +325 -325
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/coursePackage/bucketIo.ts +46 -0
- package/src/utils/coursePackage/learnJson.ts +37 -0
- package/src/utils/coursePackage/sidebar.ts +28 -0
- package/src/utils/creatorSocket.ts +16 -22
- package/src/utils/gcpCredentials.ts +38 -0
- package/src/utils/rigoActions.ts +163 -0
- package/src/utils/socketRegistry.ts +85 -0
- package/lib/creatorDist/assets/index-CjddKHB_.css +0 -1
- package/src/creatorDist/assets/index-CjddKHB_.css +0 -1
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
12
|
<title>Learnpack Creator: Craft tutorials in seconds!</title>
|
|
13
|
-
<script type="module" crossorigin src="/creator/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/creator/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/creator/assets/index-D6pmbMe9.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/creator/assets/index-zrPponAn.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/lib/models/creator.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface SyncNotification {
|
|
|
3
3
|
sourceLanguage: string;
|
|
4
4
|
createdAt: number;
|
|
5
5
|
updatedAt: number;
|
|
6
|
-
status: "pending" | "processing" | "completed" | "error";
|
|
6
|
+
status: "pending" | "processing" | "completed" | "partial" | "error";
|
|
7
7
|
processingLastUpdate?: number;
|
|
8
8
|
error?: {
|
|
9
9
|
message: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Bucket } from "@google-cloud/storage";
|
|
2
|
+
import { CourseStorage } from "../ports/courseStorage";
|
|
3
|
+
/**
|
|
4
|
+
* GCS-backed course storage.
|
|
5
|
+
*
|
|
6
|
+
* Writes go through `uploadFileToBucket` rather than `bucket.file().save()`
|
|
7
|
+
* directly: that helper sanitizes README newlines, and a second write path to
|
|
8
|
+
* the same bucket that skipped it would store content the translation step
|
|
9
|
+
* later chokes on.
|
|
10
|
+
* @param bucket - Bucket holding course drafts.
|
|
11
|
+
* @returns A CourseStorage backed by that bucket.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createGcsCourseStorage(bucket: Bucket): CourseStorage;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGcsCourseStorage = createGcsCourseStorage;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const bucketIo_1 = require("../../../utils/coursePackage/bucketIo");
|
|
6
|
+
/**
|
|
7
|
+
* GCS-backed course storage.
|
|
8
|
+
*
|
|
9
|
+
* Writes go through `uploadFileToBucket` rather than `bucket.file().save()`
|
|
10
|
+
* directly: that helper sanitizes README newlines, and a second write path to
|
|
11
|
+
* the same bucket that skipped it would store content the translation step
|
|
12
|
+
* later chokes on.
|
|
13
|
+
* @param bucket - Bucket holding course drafts.
|
|
14
|
+
* @returns A CourseStorage backed by that bucket.
|
|
15
|
+
*/
|
|
16
|
+
function createGcsCourseStorage(bucket) {
|
|
17
|
+
return {
|
|
18
|
+
async write(path, content) {
|
|
19
|
+
try {
|
|
20
|
+
await (0, bucketIo_1.uploadFileToBucket)(bucket, content, path);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new errors_1.IngestError("STORAGE_FAILURE", `Could not write ${path}`, error);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async read(path) {
|
|
27
|
+
try {
|
|
28
|
+
const [buf] = await bucket.file(path).download();
|
|
29
|
+
return buf.toString();
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
// A missing object and an unreadable one are indistinguishable here
|
|
33
|
+
// without inspecting the GCS error code; callers treat both as absent.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async list(prefix) {
|
|
38
|
+
try {
|
|
39
|
+
const [files] = await bucket.getFiles({ prefix });
|
|
40
|
+
return files.map(file => file.name);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new errors_1.IngestError("STORAGE_FAILURE", `Could not list objects under ${prefix}`, error);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
async deleteMany(paths) {
|
|
47
|
+
if (paths.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await Promise.all(paths.map(path => bucket.file(path).delete()));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new errors_1.IngestError("STORAGE_FAILURE", `Could not delete ${paths.length} object(s)`, error);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RequestAuthenticator } from "../ports/requestAuthenticator";
|
|
2
|
+
/**
|
|
3
|
+
* Authenticates a request by its Rigobot user token.
|
|
4
|
+
*
|
|
5
|
+
* Most of `serve.ts` reads this header without checking it, which turns a stale
|
|
6
|
+
* token into an opaque failure several calls deeper. One round trip up front
|
|
7
|
+
* costs little and names the problem where it happened.
|
|
8
|
+
*
|
|
9
|
+
* Note this validates rather than reusing `isValidRigoToken` from
|
|
10
|
+
* `utils/rigoActions`: that one calls Node's global fetch even though the
|
|
11
|
+
* package declares support for Node 14, and propagating that is not worth the
|
|
12
|
+
* five lines it would save.
|
|
13
|
+
* @returns A RequestAuthenticator backed by Rigobot's token endpoint.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createRigoUserTokenAuth(): RequestAuthenticator;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRigoUserTokenAuth = createRigoUserTokenAuth;
|
|
4
|
+
const axios_1 = require("axios");
|
|
5
|
+
const api_1 = require("../../../utils/api");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const TOKEN_HEADER = "x-rigo-token";
|
|
8
|
+
const readHeader = (headers, name) => {
|
|
9
|
+
var _a;
|
|
10
|
+
const raw = (_a = headers[name]) !== null && _a !== void 0 ? _a : headers[name.toLowerCase()];
|
|
11
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
12
|
+
return (value || "").trim();
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Authenticates a request by its Rigobot user token.
|
|
16
|
+
*
|
|
17
|
+
* Most of `serve.ts` reads this header without checking it, which turns a stale
|
|
18
|
+
* token into an opaque failure several calls deeper. One round trip up front
|
|
19
|
+
* costs little and names the problem where it happened.
|
|
20
|
+
*
|
|
21
|
+
* Note this validates rather than reusing `isValidRigoToken` from
|
|
22
|
+
* `utils/rigoActions`: that one calls Node's global fetch even though the
|
|
23
|
+
* package declares support for Node 14, and propagating that is not worth the
|
|
24
|
+
* five lines it would save.
|
|
25
|
+
* @returns A RequestAuthenticator backed by Rigobot's token endpoint.
|
|
26
|
+
*/
|
|
27
|
+
function createRigoUserTokenAuth() {
|
|
28
|
+
return {
|
|
29
|
+
async authenticate(headers) {
|
|
30
|
+
const rigoToken = readHeader(headers, TOKEN_HEADER);
|
|
31
|
+
if (!rigoToken) {
|
|
32
|
+
throw new errors_1.IngestError("INVALID_TOKEN", `Missing ${TOKEN_HEADER} header`);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/auth/token/${encodeURIComponent(rigoToken)}`);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (axios_1.default.isAxiosError(error) && error.response) {
|
|
39
|
+
throw new errors_1.IngestError("INVALID_TOKEN", "Rigobot rejected the token", error);
|
|
40
|
+
}
|
|
41
|
+
throw new errors_1.IngestError("REGISTRY_UNAVAILABLE", "Could not reach Rigobot to validate the token", error);
|
|
42
|
+
}
|
|
43
|
+
return { rigoToken };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PackageRegistry } from "../ports/packageRegistry";
|
|
2
|
+
/**
|
|
3
|
+
* Rigobot-backed package registry.
|
|
4
|
+
* @param rigoToken - Rigobot user token owning the packages.
|
|
5
|
+
* @returns A PackageRegistry talking to RIGOBOT_HOST.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createRigobotPackageRegistry(rigoToken: string): PackageRegistry;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRigobotPackageRegistry = createRigobotPackageRegistry;
|
|
4
|
+
const axios_1 = require("axios");
|
|
5
|
+
const api_1 = require("../../../utils/api");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
/** Rigobot rejects a duplicate slug with a 400 whose body names the conflict. */
|
|
8
|
+
const SLUG_CONFLICT_PATTERN = /already (taken|in use)/i;
|
|
9
|
+
const authHeaders = (rigoToken) => ({
|
|
10
|
+
// Tokens read from a .env file routinely carry a trailing newline, which
|
|
11
|
+
// makes the header invalid in a way the server reports as a plain 401.
|
|
12
|
+
Authorization: `Token ${rigoToken.replace(/[\n\r]/g, "").trim()}`,
|
|
13
|
+
});
|
|
14
|
+
const toRigoPackage = (data) => {
|
|
15
|
+
var _a;
|
|
16
|
+
return ({
|
|
17
|
+
id: data.id,
|
|
18
|
+
package_slug: data.package_slug,
|
|
19
|
+
organization: (_a = data.organization) !== null && _a !== void 0 ? _a : null,
|
|
20
|
+
status: data.status,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Turns an axios failure into a typed ingest failure.
|
|
25
|
+
* @param error - Whatever the request rejected with.
|
|
26
|
+
* @param context - Short description of the attempted operation.
|
|
27
|
+
* @returns Never — always throws.
|
|
28
|
+
*/
|
|
29
|
+
function rethrowAsIngestError(error, context) {
|
|
30
|
+
var _a;
|
|
31
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
32
|
+
const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
33
|
+
if (status === 401 || status === 403) {
|
|
34
|
+
throw new errors_1.IngestError("INVALID_TOKEN", `Rigobot rejected the token while ${context}`, error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new errors_1.IngestError("REGISTRY_UNAVAILABLE", `Rigobot request failed while ${context}`, error);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rigobot-backed package registry.
|
|
41
|
+
* @param rigoToken - Rigobot user token owning the packages.
|
|
42
|
+
* @returns A PackageRegistry talking to RIGOBOT_HOST.
|
|
43
|
+
*/
|
|
44
|
+
function createRigobotPackageRegistry(rigoToken) {
|
|
45
|
+
return {
|
|
46
|
+
async isSlugAvailable(slug) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/learnpack/check-slug-availability`, { params: { slug } });
|
|
49
|
+
return response.data.available === true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
rethrowAsIngestError(error, `checking availability of "${slug}"`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async createPackage(slug, config) {
|
|
56
|
+
var _a, _b, _c;
|
|
57
|
+
try {
|
|
58
|
+
const data = await api_1.default.createRigoPackage(rigoToken, slug, config);
|
|
59
|
+
return toRigoPackage(data);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 400) {
|
|
63
|
+
const detail = String((_c = (_b = error.response.data) === null || _b === void 0 ? void 0 : _b.error) !== null && _c !== void 0 ? _c : "");
|
|
64
|
+
if (SLUG_CONFLICT_PATTERN.test(detail)) {
|
|
65
|
+
throw new errors_1.IngestError("SLUG_TAKEN", `The slug "${slug}" is already registered`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
rethrowAsIngestError(error, `creating the package "${slug}"`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async getPackageBySlug(slug) {
|
|
72
|
+
var _a, _b;
|
|
73
|
+
try {
|
|
74
|
+
const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/learnpack/package/${encodeURIComponent(slug)}/`, { headers: authHeaders(rigoToken) });
|
|
75
|
+
return toRigoPackage(response.data);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Rigobot answers 404 for "no such package" and 403 for one owned by
|
|
79
|
+
// somebody else. Both mean "not yours to read"; the caller decides what
|
|
80
|
+
// that implies, so neither is an error here.
|
|
81
|
+
if (axios_1.default.isAxiosError(error) &&
|
|
82
|
+
(((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
rethrowAsIngestError(error, `reading the package "${slug}"`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async listOrganizations() {
|
|
89
|
+
var _a, _b;
|
|
90
|
+
try {
|
|
91
|
+
const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/auth/me/organization`, { headers: authHeaders(rigoToken) });
|
|
92
|
+
// Paginated endpoints answer with `results`; unpaginated ones with a
|
|
93
|
+
// bare array.
|
|
94
|
+
const rows = Array.isArray(response.data) ?
|
|
95
|
+
response.data :
|
|
96
|
+
(_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.results) !== null && _b !== void 0 ? _b : [];
|
|
97
|
+
return rows.map((row) => ({ id: row.id, name: row.name }));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
rethrowAsIngestError(error, "listing your organizations");
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async assignOrganization(packageId, organizationId) {
|
|
104
|
+
var _a, _b, _c;
|
|
105
|
+
try {
|
|
106
|
+
await axios_1.default.put(`${api_1.RIGOBOT_HOST}/v1/learnpack/organization/package/${packageId}`, { organization: organizationId }, { headers: authHeaders(rigoToken) });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// Rigobot only accepts this while the organization is still null, and
|
|
110
|
+
// answers 400 once one is set. There is no detach endpoint, so this is
|
|
111
|
+
// terminal rather than retryable.
|
|
112
|
+
if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 400) {
|
|
113
|
+
throw new errors_1.IngestError("ORGANIZATION_MISMATCH", `Package ${packageId} could not be assigned to organization ${organizationId}: ${(_c = (_b = error.response.data) === null || _b === void 0 ? void 0 : _b.error) !== null && _c !== void 0 ? _c : "it already belongs to one"}`, error);
|
|
114
|
+
}
|
|
115
|
+
rethrowAsIngestError(error, `assigning package ${packageId} to organization ${organizationId}`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ConfigResponse } from "../../../utils/configBuilder";
|
|
2
|
+
import { IngestLesson } from "./types";
|
|
3
|
+
export type BuildIngestConfigInput = {
|
|
4
|
+
lessons: IngestLesson[];
|
|
5
|
+
/** The package's learn.json, already normalized. */
|
|
6
|
+
learnJson: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Derives `.learn/config.json` without going back to the bucket.
|
|
10
|
+
*
|
|
11
|
+
* `buildConfig` (`utils/configBuilder.ts:47`) produces the same shape, but it
|
|
12
|
+
* reads the bucket for exactly two things — the object listing and learn.json —
|
|
13
|
+
* and the ingest already holds both in memory by the time it gets here. Writing
|
|
14
|
+
* the course and then reading it back to describe it would be a round trip that
|
|
15
|
+
* answers a question we already know.
|
|
16
|
+
*
|
|
17
|
+
* The file has to exist because `POST /actions/publish/:slug` reads it as its
|
|
18
|
+
* first step and fails outright when it is missing; without this, publishing a
|
|
19
|
+
* course nobody had opened in the creator would 500. `GET /config` regenerates
|
|
20
|
+
* it from the bucket on every call, so this version only has to carry publish
|
|
21
|
+
* through: its title, description and duration, plus each exercise's slug and
|
|
22
|
+
* translations. `exercise.files` is never read anywhere in the publish flow,
|
|
23
|
+
* which is why an empty list is honest rather than lazy.
|
|
24
|
+
* @param input - Paired lessons and the normalized learn.json.
|
|
25
|
+
* @returns The `{ config, exercises }` pair the bucket stores.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildIngestConfig(input: BuildIngestConfigInput): ConfigResponse;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIngestConfig = buildIngestConfig;
|
|
4
|
+
const creatorUtilities_1 = require("../../../utils/creatorUtilities");
|
|
5
|
+
/**
|
|
6
|
+
* Derives `.learn/config.json` without going back to the bucket.
|
|
7
|
+
*
|
|
8
|
+
* `buildConfig` (`utils/configBuilder.ts:47`) produces the same shape, but it
|
|
9
|
+
* reads the bucket for exactly two things — the object listing and learn.json —
|
|
10
|
+
* and the ingest already holds both in memory by the time it gets here. Writing
|
|
11
|
+
* the course and then reading it back to describe it would be a round trip that
|
|
12
|
+
* answers a question we already know.
|
|
13
|
+
*
|
|
14
|
+
* The file has to exist because `POST /actions/publish/:slug` reads it as its
|
|
15
|
+
* first step and fails outright when it is missing; without this, publishing a
|
|
16
|
+
* course nobody had opened in the creator would 500. `GET /config` regenerates
|
|
17
|
+
* it from the bucket on every call, so this version only has to carry publish
|
|
18
|
+
* through: its title, description and duration, plus each exercise's slug and
|
|
19
|
+
* translations. `exercise.files` is never read anywhere in the publish flow,
|
|
20
|
+
* which is why an empty list is honest rather than lazy.
|
|
21
|
+
* @param input - Paired lessons and the normalized learn.json.
|
|
22
|
+
* @returns The `{ config, exercises }` pair the bucket stores.
|
|
23
|
+
*/
|
|
24
|
+
function buildIngestConfig(input) {
|
|
25
|
+
const exercises = input.lessons.map((lesson, position) => {
|
|
26
|
+
const translations = {};
|
|
27
|
+
for (const lang of lesson.languages) {
|
|
28
|
+
translations[lang] = `README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
title: lesson.title,
|
|
32
|
+
slug: lesson.exerciseSlug,
|
|
33
|
+
graded: false,
|
|
34
|
+
files: [],
|
|
35
|
+
translations,
|
|
36
|
+
position,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
return { config: Object.assign({}, input.learnJson), exercises };
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Syllabus } from "../../../models/creator";
|
|
2
|
+
import { IngestLesson } from "./types";
|
|
3
|
+
export type BuildSyllabusInput = {
|
|
4
|
+
slug: string;
|
|
5
|
+
language: string;
|
|
6
|
+
lessons: IngestLesson[];
|
|
7
|
+
/** The package's `learn.json`, already normalized. */
|
|
8
|
+
learnJson: Record<string, unknown>;
|
|
9
|
+
/** Injectable clock, so tests can assert on timestamps. */
|
|
10
|
+
now?: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Builds the `initialSyllabus.json` for a course whose content already exists.
|
|
14
|
+
*
|
|
15
|
+
* The sibling of `buildSyllabusFromSources`
|
|
16
|
+
* (`utils/repair/legacyPackageRepair.ts:363`), which solves the same problem
|
|
17
|
+
* for packages that predate the creator. That one has to infer titles from an
|
|
18
|
+
* H1 and types from a heuristic; here the generator's plan already knows both,
|
|
19
|
+
* so the inference is skipped and the real values are used. The invariants
|
|
20
|
+
* below are shared between the two — change one and check the other.
|
|
21
|
+
* @param input - Paired lessons plus course metadata.
|
|
22
|
+
* @returns A syllabus the creator treats as a finished course.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildInitialSyllabus(input: BuildSyllabusInput): Syllabus;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildInitialSyllabus = buildInitialSyllabus;
|
|
4
|
+
const pickLocalized = (value, language) => {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (!value || typeof value !== "object") {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
const map = value;
|
|
12
|
+
const candidate = map[language] || Object.values(map)[0];
|
|
13
|
+
return typeof candidate === "string" ? candidate : "";
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Builds the `courseInfo` half of the syllabus.
|
|
17
|
+
*
|
|
18
|
+
* The six wizard-owned fields carry their "nothing pending" values rather than
|
|
19
|
+
* a guess: an ingested course was never authored in the creator, so there is no
|
|
20
|
+
* step to resume and no content index to honour. Same reasoning, and same
|
|
21
|
+
* values, as `buildCourseInfo` in `utils/repair/legacyPackageRepair.ts`.
|
|
22
|
+
* @param input - Course slug, language and learn.json.
|
|
23
|
+
* @returns The courseInfo object.
|
|
24
|
+
*/
|
|
25
|
+
function buildCourseInfo(input) {
|
|
26
|
+
const { learnJson, language, slug } = input;
|
|
27
|
+
const difficulty = learnJson.difficulty;
|
|
28
|
+
const duration = learnJson.duration;
|
|
29
|
+
return {
|
|
30
|
+
title: pickLocalized(learnJson.title, language) || slug,
|
|
31
|
+
description: pickLocalized(learnJson.description, language),
|
|
32
|
+
duration: typeof duration === "number" ? duration : 0,
|
|
33
|
+
difficulty: (typeof difficulty === "string" ?
|
|
34
|
+
difficulty :
|
|
35
|
+
"beginner"),
|
|
36
|
+
language,
|
|
37
|
+
technologies: Array.isArray(learnJson.technologies) ?
|
|
38
|
+
learnJson.technologies :
|
|
39
|
+
[],
|
|
40
|
+
slug,
|
|
41
|
+
hasContentIndex: false,
|
|
42
|
+
contentIndex: "",
|
|
43
|
+
isCompleted: true,
|
|
44
|
+
variables: [],
|
|
45
|
+
currentStep: "",
|
|
46
|
+
purpose: "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds the `initialSyllabus.json` for a course whose content already exists.
|
|
51
|
+
*
|
|
52
|
+
* The sibling of `buildSyllabusFromSources`
|
|
53
|
+
* (`utils/repair/legacyPackageRepair.ts:363`), which solves the same problem
|
|
54
|
+
* for packages that predate the creator. That one has to infer titles from an
|
|
55
|
+
* H1 and types from a heuristic; here the generator's plan already knows both,
|
|
56
|
+
* so the inference is skipped and the real values are used. The invariants
|
|
57
|
+
* below are shared between the two — change one and check the other.
|
|
58
|
+
* @param input - Paired lessons plus course metadata.
|
|
59
|
+
* @returns A syllabus the creator treats as a finished course.
|
|
60
|
+
*/
|
|
61
|
+
function buildInitialSyllabus(input) {
|
|
62
|
+
var _a;
|
|
63
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : Date.now();
|
|
64
|
+
const lessons = input.lessons.map(lesson => {
|
|
65
|
+
const translations = {};
|
|
66
|
+
for (const lang of lesson.languages) {
|
|
67
|
+
// Deliberately without `sourceContentHash` or `descriptionPromptVersion`:
|
|
68
|
+
// their absence is what makes `workList.needsGeneration` return true, so
|
|
69
|
+
// the descriptions backfill picks this course up on its next run.
|
|
70
|
+
//
|
|
71
|
+
// `completedAt` must be truthy — the IDE reads a `startedAt` without one
|
|
72
|
+
// as a translation still in flight and shows a spinner on a finished
|
|
73
|
+
// course. `completionId` is only ever written, never read back to query
|
|
74
|
+
// Rigobot, so zero is safe.
|
|
75
|
+
translations[lang] = {
|
|
76
|
+
completionId: 0,
|
|
77
|
+
startedAt: now,
|
|
78
|
+
completedAt: now,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
id: lesson.id,
|
|
83
|
+
// The exercise folder slug, not a random id: it is the lookup key the
|
|
84
|
+
// rest of the server matches on, and it travels inside webhook URLs.
|
|
85
|
+
uid: lesson.exerciseSlug,
|
|
86
|
+
title: lesson.title,
|
|
87
|
+
type: lesson.type,
|
|
88
|
+
description: lesson.description,
|
|
89
|
+
duration: lesson.durationMinutes,
|
|
90
|
+
// This pair is what tells the whole UI the course is finished: without
|
|
91
|
+
// `generated`, the publish button reports unreviewed lessons; without
|
|
92
|
+
// `status`, Rigobot's agent believes it may still generate content.
|
|
93
|
+
generated: true,
|
|
94
|
+
status: "DONE",
|
|
95
|
+
// Must exist even when empty — `syllabusHash` in serve.ts calls
|
|
96
|
+
// Object.entries on it with no optional chaining, on nearly every request.
|
|
97
|
+
translations,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
lessons,
|
|
102
|
+
courseInfo: buildCourseInfo(input),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TSidebar } from "../../../utils/sidebarGenerator";
|
|
2
|
+
import { IngestLesson } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Builds `sidebar.json`: `{ [exerciseSlug]: { [lang]: title } }`.
|
|
5
|
+
*
|
|
6
|
+
* Unlike `createInitialSidebar` (`utils/coursePackage/sidebar.ts`), which files
|
|
7
|
+
* each entry under its own slug as a placeholder, an ingested course already
|
|
8
|
+
* has real titles — the creator flow only uses slugs because at creation time
|
|
9
|
+
* no lesson has been written yet.
|
|
10
|
+
* @param lessons - Paired lessons, in the order they should appear.
|
|
11
|
+
* @returns The sidebar, keyed by exercise folder slug.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildSidebar(lessons: IngestLesson[]): TSidebar;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSidebar = buildSidebar;
|
|
4
|
+
/**
|
|
5
|
+
* Builds `sidebar.json`: `{ [exerciseSlug]: { [lang]: title } }`.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `createInitialSidebar` (`utils/coursePackage/sidebar.ts`), which files
|
|
8
|
+
* each entry under its own slug as a placeholder, an ingested course already
|
|
9
|
+
* has real titles — the creator flow only uses slugs because at creation time
|
|
10
|
+
* no lesson has been written yet.
|
|
11
|
+
* @param lessons - Paired lessons, in the order they should appear.
|
|
12
|
+
* @returns The sidebar, keyed by exercise folder slug.
|
|
13
|
+
*/
|
|
14
|
+
function buildSidebar(lessons) {
|
|
15
|
+
const sidebar = {};
|
|
16
|
+
for (const lesson of lessons) {
|
|
17
|
+
const entry = {};
|
|
18
|
+
for (const lang of lesson.languages) {
|
|
19
|
+
entry[lang] = lesson.title;
|
|
20
|
+
}
|
|
21
|
+
// A lesson with no README in any language would key an empty object, which
|
|
22
|
+
// reads to the IDE as a lesson with no titles at all.
|
|
23
|
+
if (Object.keys(entry).length > 0) {
|
|
24
|
+
sidebar[lesson.exerciseSlug] = entry;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return sidebar;
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each part of an ingested course lives in the bucket.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a copy of `gcsLayout` (`utils/repair/repairStorage.ts:93`)
|
|
5
|
+
* rather than an import: that module pulls in `@google-cloud/storage` and the
|
|
6
|
+
* AWS SDK at its top, and the ingest core must stay free of infrastructure.
|
|
7
|
+
*
|
|
8
|
+
* The two agreeing is load-bearing, though — the repair pass writes to the same
|
|
9
|
+
* bucket, so a disagreement about where a file belongs would have it "fix" a
|
|
10
|
+
* course the ingest had just written correctly. A test asserts the two match;
|
|
11
|
+
* that is what keeps this copy honest.
|
|
12
|
+
*/
|
|
13
|
+
export declare const COURSES_PREFIX = "courses";
|
|
14
|
+
export declare const coursePrefix: (slug: string) => string;
|
|
15
|
+
export declare const learnJsonPath: (slug: string) => string;
|
|
16
|
+
export declare const configPath: (slug: string) => string;
|
|
17
|
+
export declare const syllabusPath: (slug: string) => string;
|
|
18
|
+
export declare const sidebarPath: (slug: string) => string;
|
|
19
|
+
export declare const exercisesPrefix: (slug: string) => string;
|
|
20
|
+
export declare const artifactsPrefix: (slug: string) => string;
|
|
21
|
+
export declare const exerciseFilePath: (slug: string, exerciseSlug: string, fileName: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* The prefixes the ingest owns and may prune on overwrite.
|
|
24
|
+
*
|
|
25
|
+
* Everything else under the course belongs to somebody else: preview images the
|
|
26
|
+
* user uploaded, `package-manifest.json`, the publish journal. Deleting outside
|
|
27
|
+
* these two would destroy work the ingest never created.
|
|
28
|
+
* @param slug - Course slug.
|
|
29
|
+
* @returns Prefixes safe to prune.
|
|
30
|
+
*/
|
|
31
|
+
export declare const prunablePrefixes: (slug: string) => string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where each part of an ingested course lives in the bucket.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a copy of `gcsLayout` (`utils/repair/repairStorage.ts:93`)
|
|
6
|
+
* rather than an import: that module pulls in `@google-cloud/storage` and the
|
|
7
|
+
* AWS SDK at its top, and the ingest core must stay free of infrastructure.
|
|
8
|
+
*
|
|
9
|
+
* The two agreeing is load-bearing, though — the repair pass writes to the same
|
|
10
|
+
* bucket, so a disagreement about where a file belongs would have it "fix" a
|
|
11
|
+
* course the ingest had just written correctly. A test asserts the two match;
|
|
12
|
+
* that is what keeps this copy honest.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.prunablePrefixes = exports.exerciseFilePath = exports.artifactsPrefix = exports.exercisesPrefix = exports.sidebarPath = exports.syllabusPath = exports.configPath = exports.learnJsonPath = exports.coursePrefix = exports.COURSES_PREFIX = void 0;
|
|
16
|
+
exports.COURSES_PREFIX = "courses";
|
|
17
|
+
// Everything under one course.
|
|
18
|
+
const coursePrefix = (slug) => `${exports.COURSES_PREFIX}/${slug}/`;
|
|
19
|
+
exports.coursePrefix = coursePrefix;
|
|
20
|
+
const learnJsonPath = (slug) => `${exports.COURSES_PREFIX}/${slug}/learn.json`;
|
|
21
|
+
exports.learnJsonPath = learnJsonPath;
|
|
22
|
+
const configPath = (slug) => `${exports.COURSES_PREFIX}/${slug}/.learn/config.json`;
|
|
23
|
+
exports.configPath = configPath;
|
|
24
|
+
const syllabusPath = (slug) => `${exports.COURSES_PREFIX}/${slug}/.learn/initialSyllabus.json`;
|
|
25
|
+
exports.syllabusPath = syllabusPath;
|
|
26
|
+
const sidebarPath = (slug) => `${exports.COURSES_PREFIX}/${slug}/.learn/sidebar.json`;
|
|
27
|
+
exports.sidebarPath = sidebarPath;
|
|
28
|
+
const exercisesPrefix = (slug) => `${exports.COURSES_PREFIX}/${slug}/exercises/`;
|
|
29
|
+
exports.exercisesPrefix = exercisesPrefix;
|
|
30
|
+
// Generation artifacts kept alongside the course. Same path the streaming
|
|
31
|
+
// persistence design reserves for them, so the generator can later write here
|
|
32
|
+
// directly without a migration.
|
|
33
|
+
const artifactsPrefix = (slug) => `${exports.COURSES_PREFIX}/${slug}/.learn/artifacts/`;
|
|
34
|
+
exports.artifactsPrefix = artifactsPrefix;
|
|
35
|
+
const exerciseFilePath = (slug, exerciseSlug, fileName) => `${(0, exports.exercisesPrefix)(slug)}${exerciseSlug}/${fileName}`;
|
|
36
|
+
exports.exerciseFilePath = exerciseFilePath;
|
|
37
|
+
/**
|
|
38
|
+
* The prefixes the ingest owns and may prune on overwrite.
|
|
39
|
+
*
|
|
40
|
+
* Everything else under the course belongs to somebody else: preview images the
|
|
41
|
+
* user uploaded, `package-manifest.json`, the publish journal. Deleting outside
|
|
42
|
+
* these two would destroy work the ingest never created.
|
|
43
|
+
* @param slug - Course slug.
|
|
44
|
+
* @returns Prefixes safe to prune.
|
|
45
|
+
*/
|
|
46
|
+
const prunablePrefixes = (slug) => [
|
|
47
|
+
(0, exports.exercisesPrefix)(slug),
|
|
48
|
+
(0, exports.artifactsPrefix)(slug),
|
|
49
|
+
];
|
|
50
|
+
exports.prunablePrefixes = prunablePrefixes;
|