Skip to content

Commit 03f9bee

Browse files
committed
Add flag to the migrator for updating only the challenge counters (numOfSubmissions and numOfRegistrants)
1 parent 610714d commit 03f9bee

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

data-migration/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ LOG_LEVEL=info
120120
# Migration behavior
121121
SKIP_MISSING_REQUIRED=false
122122
USE_TRANSACTIONS=true
123+
CHALLENGE_COUNTERS_ONLY=false
123124
124125
# Migration attribution
125126
CREATED_BY=migration
@@ -130,6 +131,11 @@ Logfiles are by default stored in `logs/migration.log`
130131
It can be configured using the env variable `LOG_FILE`
131132
Log levels(increasing level of information): `error`, `warn`, `info`, `debug`
132133
Further migration configuration can also be done in `src/config.js`
134+
135+
### Updating Challenge Counters Only
136+
137+
Set `CHALLENGE_COUNTERS_ONLY=true` to re-run the `Challenge` migrator without touching other fields. In this mode the tool will skip normal validations and only update `numOfRegistrants` and `numOfSubmissions` for challenges that already exist in the database. Make sure the JSON payload still includes the challenge `id` and the counter values you want to refresh.
138+
133139
## Testing
134140
The project includes comprehensive tests to validate that data has been migrated correctly:
135141
```

data-migration/src/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ module.exports = {
2323
// Logfile path
2424
LOG_FILE: process.env.LOG_FILE || path.join(__dirname, '..', 'logs', 'migration.log'),
2525

26+
// Specialized challenge migration toggles
27+
CHALLENGE_COUNTERS_ONLY: process.env.CHALLENGE_COUNTERS_ONLY === 'true',
28+
2629
migrator: {
2730
ChallengeType: {
2831
idField: 'id',
@@ -132,6 +135,7 @@ module.exports = {
132135
createdBy: process.env.CREATED_BY || 'migration',
133136
updatedBy: process.env.UPDATED_BY || 'migration'
134137
},
138+
countersOnly: process.env.CHALLENGE_COUNTERS_ONLY === 'true',
135139
filename: process.env.CHALLENGE_FILE || 'challenge-api.challenge.json'
136140
},
137141

data-migration/src/migrators/challengeMigrator.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,86 @@ class ChallengeMigrator extends BaseMigrator {
2828
super('Challenge', 2, true);
2929
}
3030

31+
isCountersOnly() {
32+
const migratorConfig = this.manager.config.migrator?.[this.modelName] || {};
33+
return Boolean(migratorConfig.countersOnly || this.manager.config.CHALLENGE_COUNTERS_ONLY);
34+
}
35+
36+
async migrate() {
37+
if (!this.isCountersOnly()) {
38+
return await super.migrate();
39+
}
40+
41+
this.manager.logger.info('Challenge migrator running in counters-only mode (numOfRegistrants & numOfSubmissions)');
42+
43+
const rawData = await this.loadData();
44+
const data = await this.beforeMigration(rawData);
45+
46+
const idField = this.getIdField();
47+
const counters = ['numOfRegistrants', 'numOfSubmissions'];
48+
49+
let processed = 0;
50+
let skipped = 0;
51+
const errors = [];
52+
53+
for (const record of data) {
54+
const id = record[idField];
55+
if (!id) {
56+
this.manager.logger.warn('Skipping challenge record without id while updating counters');
57+
skipped++;
58+
continue;
59+
}
60+
61+
const updateData = {};
62+
63+
for (const field of counters) {
64+
const rawValue = record[field];
65+
if (rawValue === undefined || rawValue === null) {
66+
continue;
67+
}
68+
69+
const numericValue = typeof rawValue === 'string' && rawValue.trim() !== ''
70+
? Number(rawValue)
71+
: rawValue;
72+
73+
if (typeof numericValue === 'number' && Number.isFinite(numericValue)) {
74+
updateData[field] = numericValue;
75+
} else {
76+
this.manager.logger.warn(`Skipping ${field} for challenge ${id}; expected numeric value, received ${rawValue}`);
77+
}
78+
}
79+
80+
if (!Object.keys(updateData).length) {
81+
this.manager.logger.debug(`No counter updates found for challenge ${id}; skipping`);
82+
skipped++;
83+
continue;
84+
}
85+
86+
try {
87+
await this.manager.prisma[this.queryName].update({
88+
where: { [idField]: id },
89+
data: updateData
90+
});
91+
this.validIds.add(id);
92+
processed++;
93+
} catch (error) {
94+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
95+
this.manager.logger.warn(`Skipping challenge ${id}; record not found in database while updating counters`);
96+
} else {
97+
this.manager.logger.error(`Failed to update counters for challenge ${id}`, error);
98+
errors.push({ id, message: error.message });
99+
}
100+
skipped++;
101+
}
102+
}
103+
104+
await this.afterMigration({ processed, skipped, errors });
105+
106+
this.manager.logger.info(`Updated counter fields for ${processed} challenges (skipped ${skipped})`);
107+
108+
return { processed, skipped, errors };
109+
}
110+
31111
async beforeMigration(data) {
32112
// Add exisitng IDs to validIds set
33113
const existing = await this.manager.prisma[this.queryName].findMany({
@@ -227,4 +307,4 @@ class ChallengeMigrator extends BaseMigrator {
227307

228308
}
229309

230-
module.exports = { ChallengeMigrator }
310+
module.exports = { ChallengeMigrator }

0 commit comments

Comments
 (0)