@learnpack/learnpack 5.0.353 → 5.0.354
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/scripts/descriptionsS3Backfill.js +142 -22
- package/lib/utils/awsCredentials.d.ts +20 -0
- package/lib/utils/awsCredentials.js +43 -0
- package/lib/utils/descriptions/backfillEvents.d.ts +60 -0
- package/lib/utils/descriptions/backfillEvents.js +107 -0
- package/lib/utils/descriptions/generateCourseDescriptions.d.ts +7 -0
- package/lib/utils/descriptions/generateCourseDescriptions.js +3 -0
- package/lib/utils/descriptions/publishStage.js +14 -3
- package/lib/utils/descriptions/resumePublication.js +17 -2
- package/lib/utils/descriptions/s3Storage.js +13 -6
- package/lib/utils/packageManifest.d.ts +8 -0
- package/lib/utils/packageManifest.js +8 -0
- package/lib/utils/repair/legacyPackageRepair.d.ts +131 -0
- package/lib/utils/repair/legacyPackageRepair.js +492 -0
- package/lib/utils/repair/repairStorage.d.ts +68 -0
- package/lib/utils/repair/repairStorage.js +89 -0
- package/lib/utils/s3/packageManifestBackfill.js +3 -8
- package/lib/utils/s3/packageSourcesAudit.d.ts +75 -0
- package/lib/utils/s3/packageSourcesAudit.js +184 -0
- package/package.json +3 -1
- package/src/scripts/README.md +244 -0
- package/src/scripts/descriptionsS3Backfill.ts +188 -20
- package/src/utils/awsCredentials.ts +57 -0
- package/src/utils/descriptions/backfillEvents.ts +152 -0
- package/src/utils/descriptions/generateCourseDescriptions.ts +10 -0
- package/src/utils/descriptions/publishStage.ts +394 -382
- package/src/utils/descriptions/resumePublication.ts +217 -200
- package/src/utils/descriptions/s3Storage.ts +214 -206
- package/src/utils/packageManifest.ts +8 -1
- package/src/utils/repair/legacyPackageRepair.ts +731 -0
- package/src/utils/repair/repairStorage.ts +168 -0
- package/src/utils/s3/packageManifestBackfill.ts +771 -776
- package/src/utils/s3/packageSourcesAudit.ts +311 -0
|
@@ -1,206 +1,214 @@
|
|
|
1
|
-
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
|
2
|
-
import { CloudFrontClient } from "@aws-sdk/client-cloudfront"
|
|
3
|
-
import { Syllabus } from "../../models/creator"
|
|
4
|
-
import { PACKAGE_MANIFEST_REL_PATH, PackageManifest } from "../packageManifest"
|
|
5
|
-
import {
|
|
6
|
-
AwsClient,
|
|
7
|
-
fetchJsonObject,
|
|
8
|
-
fetchTextObject,
|
|
9
|
-
invalidatePackageManifestPaths,
|
|
10
|
-
listObjectKeys,
|
|
11
|
-
processPackage,
|
|
12
|
-
withRetry,
|
|
13
|
-
} from "../s3/packageManifestBackfill"
|
|
14
|
-
import { SyllabusSyncStorage } from "../syllabusSync"
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1
|
+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
|
2
|
+
import { CloudFrontClient } from "@aws-sdk/client-cloudfront"
|
|
3
|
+
import { Syllabus } from "../../models/creator"
|
|
4
|
+
import { PACKAGE_MANIFEST_REL_PATH, PackageManifest } from "../packageManifest"
|
|
5
|
+
import {
|
|
6
|
+
AwsClient,
|
|
7
|
+
fetchJsonObject,
|
|
8
|
+
fetchTextObject,
|
|
9
|
+
invalidatePackageManifestPaths,
|
|
10
|
+
listObjectKeys,
|
|
11
|
+
processPackage,
|
|
12
|
+
withRetry,
|
|
13
|
+
} from "../s3/packageManifestBackfill"
|
|
14
|
+
import { SyllabusSyncStorage } from "../syllabusSync"
|
|
15
|
+
import {
|
|
16
|
+
awsRegion,
|
|
17
|
+
requireAwsCredentials,
|
|
18
|
+
requireS3PackagesBucket,
|
|
19
|
+
} from "../awsCredentials"
|
|
20
|
+
import { CourseDescriptionsStorage } from "./generateCourseDescriptions"
|
|
21
|
+
import { CourseExercise } from "./workList"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Published-bucket (S3) adapter for the description flows.
|
|
25
|
+
*
|
|
26
|
+
* The published package is the snapshot descriptions are generated from: it is
|
|
27
|
+
* immutable between publications, unlike the draft in GCS, which keeps moving
|
|
28
|
+
* while the teacher edits.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export const syllabusKey = (slug: string) =>
|
|
32
|
+
`${slug}/.learn/initialSyllabus.json`
|
|
33
|
+
|
|
34
|
+
export const manifestKey = (slug: string) =>
|
|
35
|
+
`${slug}/${PACKAGE_MANIFEST_REL_PATH}`
|
|
36
|
+
|
|
37
|
+
const configKeys = (slug: string) => [
|
|
38
|
+
`${slug}/.learn/config.json`,
|
|
39
|
+
`${slug}/config.json`,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
type ConfigShape = {
|
|
43
|
+
exercises?: CourseExercise[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function fetchExercises(
|
|
47
|
+
s3: AwsClient,
|
|
48
|
+
bucket: string,
|
|
49
|
+
slug: string
|
|
50
|
+
): Promise<CourseExercise[]> {
|
|
51
|
+
for (const key of configKeys(slug)) {
|
|
52
|
+
// eslint-disable-next-line no-await-in-loop -- the second key is a fallback
|
|
53
|
+
const config = await fetchJsonObject<ConfigShape>(s3, bucket, key)
|
|
54
|
+
if (config?.exercises && Array.isArray(config.exercises)) {
|
|
55
|
+
return config.exercises
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createS3SyllabusSyncStorage(
|
|
63
|
+
s3: AwsClient,
|
|
64
|
+
bucket: string
|
|
65
|
+
): SyllabusSyncStorage {
|
|
66
|
+
return {
|
|
67
|
+
async listExerciseFolderFileCounts(courseSlug) {
|
|
68
|
+
const prefix = `${courseSlug}/exercises/`
|
|
69
|
+
const keys = await listObjectKeys(s3, bucket, prefix)
|
|
70
|
+
const counts = new Map<string, number>()
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
const segment = key.slice(prefix.length).split("/")[0]
|
|
73
|
+
if (segment) {
|
|
74
|
+
counts.set(segment, (counts.get(segment) || 0) + 1)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return counts
|
|
79
|
+
},
|
|
80
|
+
async translationLangsBySlug(courseSlug) {
|
|
81
|
+
const exercises = await fetchExercises(s3, bucket, courseSlug)
|
|
82
|
+
const map = new Map<string, string[]>()
|
|
83
|
+
for (const exercise of exercises) {
|
|
84
|
+
const langs = Object.keys(exercise.translations || {})
|
|
85
|
+
.map(lang => lang.toLowerCase())
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
if (langs.length > 0) {
|
|
88
|
+
map.set(exercise.slug, [...new Set(langs)])
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return map
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type S3DescriptionsStorageOptions = {
|
|
98
|
+
/** CloudFront distribution to invalidate after re-projecting the manifest. */
|
|
99
|
+
cloudFront?: { client: AwsClient; distributionId: string };
|
|
100
|
+
/** Set false in the backfill, where manifests are projected in a later pass. */
|
|
101
|
+
reprojectManifest?: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Storage for the published bucket, wired from the environment. Used by the
|
|
106
|
+
* post-publish stage and the sweep, which have no CLI flags to read.
|
|
107
|
+
*/
|
|
108
|
+
export function createS3DescriptionsStorageFromEnv(): CourseDescriptionsStorage {
|
|
109
|
+
// Validated here so a missing variable fails with its own name, instead of
|
|
110
|
+
// surfacing minutes later as "Could not load credentials from any providers"
|
|
111
|
+
// from inside a background job.
|
|
112
|
+
const credentials = requireAwsCredentials()
|
|
113
|
+
const region = awsRegion()
|
|
114
|
+
|
|
115
|
+
const s3 = new S3Client({ region, credentials }) as unknown as AwsClient
|
|
116
|
+
|
|
117
|
+
const distributionId = (process.env.CLOUDFRONT_DISTRIBUTION_ID || "").trim()
|
|
118
|
+
const cloudFront = distributionId ?
|
|
119
|
+
{
|
|
120
|
+
client: new CloudFrontClient({
|
|
121
|
+
region,
|
|
122
|
+
credentials,
|
|
123
|
+
}) as unknown as AwsClient,
|
|
124
|
+
distributionId,
|
|
125
|
+
} :
|
|
126
|
+
undefined
|
|
127
|
+
|
|
128
|
+
if (!cloudFront) {
|
|
129
|
+
console.warn(
|
|
130
|
+
"[descriptions] CLOUDFRONT_DISTRIBUTION_ID is not set: the manifest will be re-projected but not invalidated"
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return createS3DescriptionsStorage(s3, requireS3PackagesBucket(), {
|
|
135
|
+
cloudFront,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createS3DescriptionsStorage(
|
|
140
|
+
s3: AwsClient,
|
|
141
|
+
bucket: string,
|
|
142
|
+
options: S3DescriptionsStorageOptions = {}
|
|
143
|
+
): CourseDescriptionsStorage {
|
|
144
|
+
const storage: CourseDescriptionsStorage = {
|
|
145
|
+
async readSyllabus(courseSlug) {
|
|
146
|
+
return fetchJsonObject<Syllabus>(s3, bucket, syllabusKey(courseSlug))
|
|
147
|
+
},
|
|
148
|
+
async writeSyllabus(courseSlug, syllabus) {
|
|
149
|
+
await withRetry(() =>
|
|
150
|
+
s3.send(
|
|
151
|
+
new PutObjectCommand({
|
|
152
|
+
Bucket: bucket,
|
|
153
|
+
Key: syllabusKey(courseSlug),
|
|
154
|
+
Body: JSON.stringify(syllabus, null, 2),
|
|
155
|
+
ContentType: "application/json",
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
async listExercises(courseSlug) {
|
|
161
|
+
return fetchExercises(s3, bucket, courseSlug)
|
|
162
|
+
},
|
|
163
|
+
async readReadme(courseSlug, exerciseSlug, fileName) {
|
|
164
|
+
return fetchTextObject(
|
|
165
|
+
s3,
|
|
166
|
+
bucket,
|
|
167
|
+
`${courseSlug}/exercises/${exerciseSlug}/${fileName}`
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
async readManifest(courseSlug) {
|
|
171
|
+
return fetchJsonObject<PackageManifest>(
|
|
172
|
+
s3,
|
|
173
|
+
bucket,
|
|
174
|
+
manifestKey(courseSlug)
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
syllabusSyncStorage: createS3SyllabusSyncStorage(s3, bucket),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.reprojectManifest !== false) {
|
|
181
|
+
storage.reprojectManifest = async courseSlug => {
|
|
182
|
+
// Preserve the publication timestamp: this is a re-projection of an
|
|
183
|
+
// already published package, not a new publication.
|
|
184
|
+
const existing = await fetchJsonObject<PackageManifest>(
|
|
185
|
+
s3,
|
|
186
|
+
bucket,
|
|
187
|
+
manifestKey(courseSlug)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const result = await processPackage(s3, bucket, courseSlug, {
|
|
191
|
+
dryRun: false,
|
|
192
|
+
force: false,
|
|
193
|
+
skipExisting: false,
|
|
194
|
+
publishedAt: existing?.publishedAt ?? null,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (result.status === "failed") {
|
|
198
|
+
throw new Error(result.error || "manifest projection failed")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (result.status === "skipped" || !options.cloudFront) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await invalidatePackageManifestPaths(
|
|
206
|
+
options.cloudFront.client,
|
|
207
|
+
options.cloudFront.distributionId,
|
|
208
|
+
[`/${manifestKey(courseSlug)}`]
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return storage
|
|
214
|
+
}
|
|
@@ -94,7 +94,14 @@ function findSyllabusLesson(
|
|
|
94
94
|
})
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Lesson id when no syllabus entry matches: the numeric prefix of the folder.
|
|
99
|
+
*
|
|
100
|
+
* Exported because `legacyPackageRepair` has to synthesize syllabus ids with
|
|
101
|
+
* exactly this rule — a repaired package whose ids differed from the ones its
|
|
102
|
+
* manifest already carries would look like a content change to every consumer.
|
|
103
|
+
*/
|
|
104
|
+
export function deriveLessonId(exerciseSlug: string): string {
|
|
98
105
|
const match = exerciseSlug.match(/^(\d+(?:\.\d+)?)/)
|
|
99
106
|
return match ? match[1] : exerciseSlug
|
|
100
107
|
}
|