Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions bin/syncReadingStreakAchievements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import '../src/config';
import { In } from 'typeorm';
import createOrGetConnection from '../src/db';
import { Achievement, AchievementEventType, UserStreak } from '../src/entity';
import { updateUserAchievementProgress } from '../src/common/achievement';
import { logger } from '../src/logger';

/**
* One-off backfill to retroactively unlock the newly added reading streak
* achievements for users whose max streak already qualifies.
* Idempotent: updateUserAchievementProgress skips already-unlocked achievements.
*/

const NEW_ACHIEVEMENT_NAMES = ['What time is it?', 'Devil is impressed'];

const start = async (): Promise<void> => {
const con = await createOrGetConnection();

try {
const achievements = await con.getRepository(Achievement).find({
where: {
eventType: AchievementEventType.ReadingStreak,
name: In(NEW_ACHIEVEMENT_NAMES),
},
});

const foundNames = new Set(achievements.map(({ name }) => name));
const missing = NEW_ACHIEVEMENT_NAMES.filter(
(name) => !foundNames.has(name),
);
if (missing.length > 0) {
throw new Error(
`Missing reading streak achievements: ${missing.join(', ')}. Run migrations first.`,
);
}

const minTarget = Math.min(
...achievements.map(({ criteria }) => criteria.targetCount ?? 1),
);

const streaks = await con
.getRepository(UserStreak)
.createQueryBuilder('us')
.select(['us.userId', 'us.maxStreak'])
.where('us.maxStreak >= :minTarget', { minTarget })
.getMany();

console.log(
`Found ${streaks.length} users with maxStreak >= ${minTarget} and ${achievements.length} target achievements.`,
);

let totalUnlocked = 0;

for (const { userId, maxStreak } of streaks) {
for (const achievement of achievements) {
const wasUnlocked = await updateUserAchievementProgress(
con,
logger,
userId,
achievement.id,
maxStreak,
achievement.criteria.targetCount ?? 1,
);

if (wasUnlocked) {
totalUnlocked++;
}
}
}

console.log(
`Finished reading streak achievements backfill. Unlocked ${totalUnlocked} achievements.`,
);
} finally {
await con.destroy();
}
};

start()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});
46 changes: 46 additions & 0 deletions src/migration/1783416206466-AddMoreReadingStreakAchievements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddMoreReadingStreakAchievements1783416206466 implements MigrationInterface {
name = 'AddMoreReadingStreakAchievements1783416206466';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(/* sql */ `
INSERT INTO "achievement" (
"name",
"description",
"image",
"type",
"eventType",
"criteria",
"points"
)
VALUES
(
'What time is it?',
'Reach a 67-day reading streak',
'https://media.daily.dev/image/upload/s--2bqSyiqr--/v1783416166/achievements/What_time_is_it',
'milestone',
'reading_streak',
'{"targetCount": 67}',
35
),
(
'Devil is impressed',
'Reach a 666-day reading streak',
'https://media.daily.dev/image/upload/s--W0-BqBQd--/v1783416167/achievements/Devil_is_impressed',
'milestone',
'reading_streak',
'{"targetCount": 666}',
50
)
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(/* sql */ `
DELETE FROM "achievement"
WHERE "eventType" = 'reading_streak'
AND "name" IN ('What time is it?', 'Devil is impressed')
`);
}
}
Loading