Skip to content

Commit b62c4ac

Browse files
author
Rajat
committed
new script: domain bulk delete; refresh media script fixes
1 parent cae6c39 commit b62c4ac

File tree

5 files changed

+185
-30
lines changed

5 files changed

+185
-30
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ report*.json
4141
.npmrc
4242

4343
# Jest files
44-
globalConfig.json
44+
globalConfig.json
45+
46+
# CourseLit files
47+
domains_to_delete.txt

apps/web/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

bulk-cleanup-domains.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
3+
# Tool to run the domain cleanup script for a list of domains provided in a file.
4+
# Each domain should be on a new line in the input file.
5+
6+
if [ -z "$1" ]; then
7+
echo "Usage: $0 <path_to_domains_list_file>"
8+
exit 1
9+
fi
10+
11+
FILE=$1
12+
13+
if [ ! -f "$FILE" ]; then
14+
echo "Error: File $FILE not found."
15+
exit 1
16+
fi
17+
18+
echo "Starting bulk cleanup..."
19+
20+
while IFS= read -r domain || [ -n "$domain" ]; do
21+
# Trim potential whitespace or carriage returns (common in files exported from Excel)
22+
domain=$(echo "$domain" | tr -d '\r' | xargs)
23+
24+
if [ -z "$domain" ]; then
25+
continue
26+
fi
27+
28+
echo "--------------------------------------------------"
29+
echo "Cleaning up domain: $domain"
30+
pnpm --filter @courselit/scripts domain:cleanup "$domain"
31+
done < "$FILE"
32+
33+
echo "--------------------------------------------------"
34+
echo "Bulk cleanup process completed."

packages/scripts/src/cleanup-domain.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* Deletes a domain and all its associated data.
3+
*
4+
* Usage: pnpm --filter @courselit/scripts domain:cleanup <domain-name>
5+
*/
16
import mongoose from "mongoose";
27
import {
38
CourseSchema,
@@ -39,11 +44,8 @@ import type {
3944
import { loadEnvFile } from "node:process";
4045
import { MediaLit } from "medialit";
4146
import { extractMediaIDs } from "@courselit/utils";
42-
import CommonModels, {
43-
Constants,
44-
ScormContent,
45-
} from "@courselit/common-models";
46-
const { CommunityMediaTypes } = CommonModels;
47+
import CommonModels from "@courselit/common-models";
48+
const { CommunityMediaTypes, Constants } = CommonModels;
4749

4850
function getMediaLitClient() {
4951
const medialit = new MediaLit({
@@ -167,6 +169,7 @@ async function cleanupDomain(name: string) {
167169
await deleteMedia(mediaId);
168170
}
169171
await DomainModel.deleteOne({ _id: domain._id });
172+
console.log(`✅ Deleted: ${name}`);
170173
}
171174

172175
async function deleteProduct({
@@ -264,10 +267,12 @@ async function deleteLessons(id: string, domain: mongoose.Types.ObjectId) {
264267
if (
265268
lesson.type === Constants.LessonType.SCORM &&
266269
lesson.content &&
267-
(lesson.content as ScormContent).mediaId
270+
(lesson.content as CommonModels.ScormContent).mediaId
268271
) {
269272
cleanupTasks.push(
270-
deleteMedia((lesson.content as ScormContent).mediaId!),
273+
deleteMedia(
274+
(lesson.content as CommonModels.ScormContent).mediaId!,
275+
),
271276
);
272277
}
273278
}

packages/scripts/src/refresh-media.ts

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
* the latest URLs from MediaLit service using the stored mediaId.
66
*
77
* Usage:
8-
* pnpm media:refresh [domain-name] [--discover]
8+
* pnpm media:refresh [domain-name] [--save]
99
*
1010
* Options:
11-
* --discover Only print all media objects found, without fetching or updating
11+
* --save Actually update the database (Default is DRY RUN / DISCOVER)
1212
*
1313
* If domain-name is provided, only that domain's media is refreshed.
1414
* If omitted, all domains are processed.
1515
*
1616
* Environment variables required:
1717
* - DB_CONNECTION_STRING: MongoDB connection string
18-
* - MEDIALIT_SERVER: MediaLit API server URL (not required for --discover)
19-
* - MEDIALIT_APIKEY: MediaLit API key (not required for --discover)
18+
* - MEDIALIT_SERVER: MediaLit API server URL
19+
* - MEDIALIT_APIKEY: MediaLit API key
2020
*/
2121

2222
import mongoose from "mongoose";
@@ -51,19 +51,17 @@ loadEnvFile();
5151

5252
// Parse command line arguments
5353
const args = process.argv.slice(2);
54-
const discoverMode = args.includes("--discover");
54+
const saveMode = args.includes("--save");
55+
const discoverMode = !saveMode;
5556
const domainArg = args.find((arg) => !arg.startsWith("--"));
5657

5758
if (!process.env.DB_CONNECTION_STRING) {
5859
throw new Error("DB_CONNECTION_STRING is not set");
5960
}
6061

61-
if (
62-
!discoverMode &&
63-
(!process.env.MEDIALIT_SERVER || !process.env.MEDIALIT_APIKEY)
64-
) {
62+
if (!process.env.MEDIALIT_SERVER || !process.env.MEDIALIT_APIKEY) {
6563
throw new Error(
66-
"MEDIALIT_SERVER and MEDIALIT_APIKEY must be set (not required for --discover mode)",
64+
"MEDIALIT_SERVER and MEDIALIT_APIKEY must be set to fetch refreshed URLs",
6765
);
6866
}
6967

@@ -89,6 +87,29 @@ const stats = {
8987
// Cache to avoid duplicate API calls for the same mediaId
9088
const mediaCache = new Map<string, Media | null>();
9189

90+
/**
91+
* Extracts Media ID from a MediaLit URL
92+
*/
93+
function extractIdFromUrl(url: string): string | null {
94+
try {
95+
const { pathname } = new URL(url);
96+
const segments = pathname.split("/").filter(Boolean);
97+
98+
if (segments.length < 2) {
99+
return null;
100+
}
101+
102+
const lastSegment = segments[segments.length - 1];
103+
if (!/^main\.[^/]+$/i.test(lastSegment)) {
104+
return null;
105+
}
106+
107+
return segments[segments.length - 2] || null;
108+
} catch {
109+
return null;
110+
}
111+
}
112+
92113
/**
93114
* Fetch fresh media data from MediaLit
94115
*/
@@ -124,12 +145,32 @@ async function refreshMediaObject(
124145

125146
stats.processed++;
126147

127-
// In discover mode, just print and return null (no update)
148+
// In discover mode, fetch and print comparison but return null
128149
if (discoverMode) {
129150
console.log(` 📎 ${context || "Media"}: ${existingMedia.mediaId}`);
130-
console.log(` File: ${existingMedia.file || "(none)"}`);
151+
console.log(
152+
` 📄 Current File: ${existingMedia.file || "(none)"}`,
153+
);
131154
if (existingMedia.thumbnail) {
132-
console.log(` Thumb: ${existingMedia.thumbnail}`);
155+
console.log(` 🖼️ Current Thumb: ${existingMedia.thumbnail}`);
156+
}
157+
158+
const freshMedia = await fetchMediaFromMediaLit(existingMedia.mediaId);
159+
if (freshMedia) {
160+
if (freshMedia.file !== existingMedia.file) {
161+
console.log(` ✨ New File: ${freshMedia.file}`);
162+
}
163+
if (freshMedia.thumbnail !== existingMedia.thumbnail) {
164+
console.log(` ✨ New Thumb: ${freshMedia.thumbnail}`);
165+
}
166+
if (
167+
freshMedia.file === existingMedia.file &&
168+
freshMedia.thumbnail === existingMedia.thumbnail
169+
) {
170+
console.log(` ✅ URLs are already up to date`);
171+
}
172+
} else {
173+
console.log(` ❌ Could not fetch updated URLs from MediaLit`);
133174
}
134175
return null;
135176
}
@@ -206,7 +247,7 @@ async function recursiveMediaRefresh(obj: any): Promise<boolean> {
206247
if (Object.prototype.hasOwnProperty.call(obj, key)) {
207248
const value = obj[key];
208249

209-
// Special handling for stringified JSON fields
250+
// 1. Special handling for stringified JSON fields (e.g. ProseMirror docs)
210251
if (
211252
typeof value === "string" &&
212253
((value.trim().startsWith("{") && value.trim().endsWith("}")) ||
@@ -220,12 +261,63 @@ async function recursiveMediaRefresh(obj: any): Promise<boolean> {
220261
obj[key] = JSON.stringify(parsed);
221262
updated = true;
222263
}
223-
continue; // Skip normal recursion for this key
264+
continue; // Skip normal recursion/string check for this key
224265
} catch {
225266
// Not valid JSON
226267
}
227268
}
228269

270+
// 2. If the value is a string, check if it's a MediaLit URL that needs refreshing
271+
if (typeof value === "string") {
272+
const extractedId = extractIdFromUrl(value);
273+
if (extractedId) {
274+
stats.processed++;
275+
if (discoverMode) {
276+
console.log(
277+
` 📎 URL found in key '${key}': ${extractedId}`,
278+
);
279+
console.log(` 🔗 Current Value: ${value}`);
280+
281+
const freshMedia =
282+
await fetchMediaFromMediaLit(extractedId);
283+
if (freshMedia) {
284+
if (freshMedia.file !== value) {
285+
console.log(
286+
` ✨ New Value: ${freshMedia.file}`,
287+
);
288+
} else {
289+
console.log(
290+
` ✅ Value is already up to date`,
291+
);
292+
}
293+
} else {
294+
console.log(
295+
` ❌ Could not fetch updated URL from MediaLit`,
296+
);
297+
}
298+
continue;
299+
}
300+
301+
const freshMedia =
302+
await fetchMediaFromMediaLit(extractedId);
303+
if (freshMedia && freshMedia.file !== value) {
304+
// Check if we should use file or thumbnail URL
305+
// Usually for 'src' we use file URL.
306+
// If the current URL ends with main.png (or similar), we update it to freshMedia.file
307+
// Note: Our extractIdFromUrl only matches main images anyway.
308+
obj[key] = freshMedia.file;
309+
updated = true;
310+
stats.updated++;
311+
} else if (!freshMedia) {
312+
stats.failed++;
313+
} else {
314+
stats.skipped++;
315+
}
316+
continue;
317+
}
318+
}
319+
320+
// 3. Normal recursion for objects/arrays
229321
const result = await recursiveMediaRefresh(value);
230322
if (result) {
231323
updated = true;
@@ -290,8 +382,17 @@ async function refreshCourseMedia(domainId: mongoose.Types.ObjectId) {
290382
}
291383

292384
if (hasUpdates) {
293-
await CourseModel.updateOne({ _id: course.id }, { $set: updates });
294-
console.log(` ✓ Course: ${course.title}`);
385+
const result = await CourseModel.updateOne(
386+
{ _id: (course as any)._id },
387+
{ $set: updates },
388+
);
389+
if (result.matchedCount === 0) {
390+
console.error(
391+
` ✗ Failed to update course: ${course.title} (No document found)`,
392+
);
393+
} else {
394+
console.log(` ✓ Course: ${course.title}`);
395+
}
295396
}
296397
}
297398
}
@@ -331,8 +432,17 @@ async function refreshLessonMedia(domainId: mongoose.Types.ObjectId) {
331432
}
332433

333434
if (hasUpdates) {
334-
await LessonModel.updateOne({ _id: lesson.id }, { $set: updates });
335-
console.log(` ✓ Lesson: ${lesson.title}`);
435+
const result = await LessonModel.updateOne(
436+
{ _id: (lesson as any)._id },
437+
{ $set: updates },
438+
);
439+
if (result.matchedCount === 0) {
440+
console.error(
441+
` ✗ Failed to update lesson: ${lesson.title} (No document found)`,
442+
);
443+
} else {
444+
console.log(` ✓ Lesson: ${lesson.title}`);
445+
}
336446
}
337447
}
338448
}
@@ -353,7 +463,7 @@ async function refreshUserMedia(domainId: mongoose.Types.ObjectId) {
353463
const updatedMedia = await refreshMediaObject(user.avatar);
354464
if (updatedMedia) {
355465
await UserModel.updateOne(
356-
{ _id: user._id },
466+
{ _id: (user as any)._id },
357467
{ $set: { avatar: updatedMedia } },
358468
);
359469
console.log(` ✓ User: ${user.email}`);
@@ -700,8 +810,11 @@ async function main() {
700810

701811
if (discoverMode) {
702812
console.log(
703-
"🔍 DISCOVER MODE: Only printing media objects, no updates\n",
813+
"🔍 DRY RUN (Discover Mode): Showing changes without updating the database.",
704814
);
815+
console.log(" To apply changes, run with --save\n");
816+
} else {
817+
console.log("💾 SAVE MODE: Updating database with fresh URLs\n");
705818
}
706819

707820
if (domainArg) {

0 commit comments

Comments
 (0)