Skip to content

Commit 9c292a5

Browse files
feat: add migrate script for exam-env collections (#232)
1 parent 2d21831 commit 9c292a5

16 files changed

+743
-0
lines changed

database/migrate/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

database/migrate/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Migrate
2+
3+
Migrates schemas in the `freecodecamp` database.
4+
5+
```bash
6+
bun install
7+
cp sample.env .env
8+
bun run index.ts
9+
```

database/migrate/bun.lock

Lines changed: 173 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Db, ObjectId } from 'mongodb';
2+
import { log } from '../logger';
3+
4+
interface ExamCreatorExam {
5+
_id: ObjectId;
6+
config: {
7+
totalTimeInMS: number;
8+
totalTimeInS?: number | null;
9+
retakeTimeInMS: number;
10+
retakeTimeInS?: number | null;
11+
};
12+
version: number;
13+
}
14+
15+
export async function migrate(db: Db) {
16+
const child = log.child({ collection: 'ExamCreatorExam' });
17+
child.info('Starting migration for ExamCreatorExam collection');
18+
const collection = db.collection<ExamCreatorExam>('ExamCreatorExam');
19+
20+
const query = {
21+
$or: [
22+
{ 'config.totalTimeInS': { $exists: false } },
23+
{ 'config.retakeTimeInS': { $exists: false } }
24+
],
25+
version: 1
26+
};
27+
28+
const cursor = collection.find(query, {
29+
projection: { _id: 1, config: 1 }
30+
});
31+
32+
const updates: {
33+
_id: ExamCreatorExam['_id'];
34+
totalTimeInS: number;
35+
retakeTimeInS: number;
36+
}[] = [];
37+
38+
while (await cursor.hasNext()) {
39+
const doc = await cursor.next();
40+
if (!doc) break;
41+
42+
const totalTimeInMS = doc.config.totalTimeInMS;
43+
const totalTimeInS = Math.round(totalTimeInMS / 1000);
44+
45+
const retakeTimeInMS = doc.config.retakeTimeInMS;
46+
const retakeTimeInS = Math.round(retakeTimeInMS / 1000);
47+
48+
updates.push({ _id: doc._id, totalTimeInS, retakeTimeInS });
49+
}
50+
51+
child.info(`Found ${updates.length} docs to migrate.`);
52+
53+
if (!updates.length) {
54+
return;
55+
}
56+
57+
// Perform updates in bulk for efficiency
58+
const bulk = collection.initializeUnorderedBulkOp();
59+
for (const u of updates) {
60+
bulk.find({ _id: u._id, ...query }).updateOne({
61+
$set: {
62+
'config.totalTimeInS': u.totalTimeInS,
63+
'config.retakeTimeInS': u.retakeTimeInS,
64+
version: 2
65+
}
66+
});
67+
}
68+
69+
const result = await bulk.execute();
70+
child.info(
71+
`Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}`
72+
);
73+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Db, ObjectId } from "mongodb";
2+
import { log } from "../logger";
3+
4+
interface ExamCreatorUser {
5+
_id: ObjectId;
6+
settings?: ExamCreatorUserSettings;
7+
version: number;
8+
}
9+
10+
interface ExamCreatorUserSettings {
11+
databaseEnvironment: "Production" | "Staging";
12+
}
13+
14+
export async function migrate(db: Db) {
15+
const child = log.child({ collection: "ExamCreatorUser" });
16+
child.info("Starting migration for ExamCreatorUser collection");
17+
const collection = db.collection<ExamCreatorUser>("ExamCreatorUser");
18+
19+
const query = {
20+
$or: [{ settings: { $exists: false } }, { version: { $exists: false } }],
21+
};
22+
23+
const cursor = collection.find(query, {
24+
projection: { _id: 1, settings: 1 },
25+
});
26+
27+
const updates: {
28+
_id: ExamCreatorUser["_id"];
29+
settings: ExamCreatorUserSettings;
30+
}[] = [];
31+
32+
while (await cursor.hasNext()) {
33+
const doc = await cursor.next();
34+
if (!doc) break;
35+
36+
const settings: ExamCreatorUserSettings = {
37+
databaseEnvironment: doc.settings?.databaseEnvironment || "Production",
38+
};
39+
40+
updates.push({ _id: doc._id, settings });
41+
}
42+
43+
child.info(`Found ${updates.length} docs to migrate.`);
44+
45+
if (!updates.length) {
46+
return;
47+
}
48+
49+
// Perform updates in bulk for efficiency
50+
const bulk = collection.initializeUnorderedBulkOp();
51+
for (const u of updates) {
52+
bulk.find({ _id: u._id, ...query }).updateOne({
53+
$set: {
54+
settings: u.settings,
55+
version: 1,
56+
},
57+
});
58+
}
59+
60+
const result = await bulk.execute();
61+
child.info(
62+
`Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}`
63+
);
64+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Db, ObjectId } from 'mongodb';
2+
import { log } from '../logger';
3+
4+
interface ExamEnvironmentChallenge {
5+
_id: ObjectId;
6+
version?: number;
7+
}
8+
9+
export async function migrate(db: Db) {
10+
const child = log.child({ collection: 'ExamEnvironmentChallenge' });
11+
child.info('Starting migration for ExamEnvironmentChallenge collection');
12+
const collection = db.collection<ExamEnvironmentChallenge>(
13+
'ExamEnvironmentChallenge'
14+
);
15+
16+
const query = {
17+
version: { $exists: false }
18+
};
19+
20+
const cursor = collection.find(query, {
21+
projection: { _id: 1 }
22+
});
23+
24+
const updates: {
25+
_id: ExamEnvironmentChallenge['_id'];
26+
version: number;
27+
}[] = [];
28+
29+
while (await cursor.hasNext()) {
30+
const doc = await cursor.next();
31+
if (!doc) break;
32+
33+
updates.push({ _id: doc._id, version: 1 });
34+
}
35+
36+
child.info(`Found ${updates.length} docs to migrate.`);
37+
38+
if (!updates.length) {
39+
return;
40+
}
41+
42+
// Perform updates in bulk for efficiency
43+
const bulk = collection.initializeUnorderedBulkOp();
44+
for (const u of updates) {
45+
bulk.find({ _id: u._id, ...query }).updateOne({
46+
$set: {
47+
version: u.version
48+
}
49+
});
50+
}
51+
52+
const result = await bulk.execute();
53+
child.info(
54+
`Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}`
55+
);
56+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Db, ObjectId } from 'mongodb';
2+
import { log } from '../logger';
3+
4+
interface ExamEnvironmentExamAttempt {
5+
_id: ObjectId;
6+
questionSets: ExamEnvironmentQuestionSetAttempt[];
7+
startTimeInMS: number;
8+
startTime?: Date;
9+
version: number;
10+
}
11+
12+
interface ExamEnvironmentQuestionSetAttempt {
13+
id: ObjectId;
14+
questions: ExamEnvironmentMultipleChoiceQuestionAttempt[];
15+
}
16+
17+
interface ExamEnvironmentMultipleChoiceQuestionAttempt {
18+
id: ObjectId;
19+
submissionTimeInMS: number;
20+
submissionTime?: Date;
21+
}
22+
23+
export async function migrate(db: Db) {
24+
const child = log.child({ collection: 'ExamEnvironmentExamAttempt' });
25+
child.info('Starting migration for ExamEnvironmentExamAttempt collection');
26+
const collection = db.collection<ExamEnvironmentExamAttempt>(
27+
'ExamEnvironmentExamAttempt'
28+
);
29+
30+
const query = {
31+
$and: [
32+
{ startTime: { $exists: false } },
33+
{ 'questionSets.questions.submissionTime': { $exists: false } }
34+
],
35+
version: 1
36+
};
37+
38+
const cursor = collection.find(query, {
39+
projection: { _id: 1, questionSets: 1, startTimeInMS: 1 }
40+
});
41+
42+
const updates: {
43+
_id: ExamEnvironmentExamAttempt['_id'];
44+
questionSets: ExamEnvironmentQuestionSetAttempt[];
45+
startTime: Date;
46+
}[] = [];
47+
48+
while (await cursor.hasNext()) {
49+
const doc = await cursor.next();
50+
if (!doc) break;
51+
52+
// Add startTime
53+
const startTime = new Date(doc.startTimeInMS);
54+
55+
for (const qs of doc.questionSets) {
56+
for (const q of qs.questions) {
57+
// Add submissionTime
58+
q.submissionTime = new Date(q.submissionTimeInMS);
59+
}
60+
}
61+
62+
updates.push({
63+
_id: doc._id,
64+
startTime,
65+
questionSets: doc.questionSets
66+
});
67+
}
68+
69+
child.info(`Found ${updates.length} docs to migrate.`);
70+
71+
if (!updates.length) {
72+
return;
73+
}
74+
75+
// Perform updates in bulk for efficiency
76+
const bulk = collection.initializeUnorderedBulkOp();
77+
for (const u of updates) {
78+
bulk.find({ _id: u._id, ...query }).updateOne({
79+
$set: {
80+
questionSets: u.questionSets,
81+
startTime: u.startTime,
82+
version: 2
83+
}
84+
});
85+
}
86+
87+
const result = await bulk.execute();
88+
child.info(
89+
`Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}`
90+
);
91+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Db, ObjectId } from 'mongodb';
2+
import { log } from '../logger';
3+
4+
interface ExamEnvironmentExamModeration {
5+
_id: ObjectId;
6+
challengesAwarded?: boolean;
7+
}
8+
9+
export async function migrate(db: Db) {
10+
const child = log.child({ collection: 'ExamEnvironmentExamModeration' });
11+
child.info('Starting migration for ExamEnvironmentExamModeration collection');
12+
const collection = db.collection<ExamEnvironmentExamModeration>(
13+
'ExamEnvironmentExamModeration'
14+
);
15+
16+
const query = {
17+
challengesAwarded: { $exists: false },
18+
version: 1
19+
};
20+
21+
const cursor = collection.find(query, {
22+
projection: { _id: 1 }
23+
});
24+
25+
const updates: {
26+
_id: ExamEnvironmentExamModeration['_id'];
27+
challengesAwarded: boolean;
28+
}[] = [];
29+
30+
while (await cursor.hasNext()) {
31+
const doc = await cursor.next();
32+
if (!doc) break;
33+
34+
updates.push({ _id: doc._id, challengesAwarded: false });
35+
}
36+
37+
child.info(`Found ${updates.length} docs to migrate.`);
38+
39+
if (!updates.length) {
40+
return;
41+
}
42+
43+
// Perform updates in bulk for efficiency
44+
const bulk = collection.initializeUnorderedBulkOp();
45+
for (const u of updates) {
46+
bulk.find({ _id: u._id, ...query }).updateOne({
47+
$set: {
48+
challengesAwarded: u.challengesAwarded,
49+
version: 2
50+
}
51+
});
52+
}
53+
54+
const result = await bulk.execute();
55+
child.info(
56+
`Bulk update complete. Matched: ${result.matchedCount}, Modified: ${result.modifiedCount}`
57+
);
58+
}

0 commit comments

Comments
 (0)