From 4afc7a19816512830f7c8cd87851ad6545100c86 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:06:29 -0300 Subject: [PATCH 1/5] Add manual migration to secret imports rework --- ...25144940_fix-secret-reminders-migration.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts new file mode 100644 index 000000000..81e8ccf31 --- /dev/null +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-await-in-loop */ +import { Knex } from "knex"; + +import { chunkArray } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; + +import { TableName } from "../schemas"; +import { TReminders, TRemindersInsert } from "../schemas/reminders"; + +export async function up(knex: Knex): Promise { + logger.info("Initializing secret reminders migration"); + const hasReminderTable = await knex.schema.hasTable(TableName.Reminder); + + if (hasReminderTable) { + const secretsWithLatestVersions = await knex(TableName.SecretV2) + .whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`) + .whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`) + .innerJoin(TableName.SecretVersionV2, (qb) => { + void qb + .on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`) + .andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`); + }) + .whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => { + void qb + .select(["secretId", knex.raw("MAX(version) as version")]) + .from(`${TableName.SecretVersionV2} as v2`) + .whereNotNull("v2.reminderRepeatDays") + .whereRaw(`"v2"."reminderRepeatDays" > 0`) + .groupBy("v2.secretId"); + }) + .select( + knex.ref("id").withSchema(TableName.SecretV2).as("secretId"), + knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"), + knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"), + knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt") + ); + + logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`); + + const reminderInserts: TRemindersInsert[] = []; + if (secretsWithLatestVersions.length > 0) { + secretsWithLatestVersions.forEach((secret) => { + if (!secret.reminderRepeatDays) return; + const nextReminderDate = new Date(secret.createdAt); + nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays); + + reminderInserts.push({ + secretId: secret.secretId, + message: secret.reminderNote, + repeatDays: secret.reminderRepeatDays, + nextReminderDate + }); + }); + + const commitBatches = chunkArray(reminderInserts, 9000); + for (const commitBatch of commitBatches) { + const insertedReminders = (await knex + .batchInsert(TableName.Reminder, commitBatch) + .returning("*")) as TReminders[]; + + const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean); + + const recipients = await knex(TableName.SecretReminderRecipients) + .whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [ + TableName.SecretReminderRecipients, + "secretId", + ...insertedReminderSecretIds + ]) + .select( + knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"), + knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId") + ); + const reminderRecipients = recipients.map((recipient) => ({ + reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id, + userId: recipient.userId + })); + + const filteredRecipients = reminderRecipients.filter((recipient) => !!recipient.reminderId); + await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients); + } + logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`); + } + + logger.info("Secret reminders migration completed"); + } else { + logger.warn("Reminder table does not exist, skipping migration"); + } +} + +export async function down(): Promise { + logger.info("Rollback not implemented for secret reminders fix migration"); +} From cd718488000e1c52309bac9d0d33e7657c387baf Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:10:54 -0300 Subject: [PATCH 2/5] Avoid migrating existing reminders --- .../20250725144940_fix-secret-reminders-migration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 81e8ccf31..35612f2d7 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -28,6 +28,10 @@ export async function up(knex: Knex): Promise { .whereRaw(`"v2"."reminderRepeatDays" > 0`) .groupBy("v2.secretId"); }) + // Add LEFT JOIN with Reminder table to check for existing reminders + .leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`) + // Only include secrets that don't already have reminders + .whereNull(`${TableName.Reminder}.secretId`) .select( knex.ref("id").withSchema(TableName.SecretV2).as("secretId"), knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"), From af32948a05b472ba0cab4547e30796604922a75f Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 13:35:06 -0300 Subject: [PATCH 3/5] Minor improvements on reminders migration --- .../20250725144940_fix-secret-reminders-migration.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 35612f2d7..9a0394bf9 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -22,10 +22,12 @@ export async function up(knex: Knex): Promise { }) .whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => { void qb - .select(["secretId", knex.raw("MAX(version) as version")]) + .select(["v2.secretId", knex.raw("MIN(v2.version) as version")]) .from(`${TableName.SecretVersionV2} as v2`) + .innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id") + .whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`) .whereNotNull("v2.reminderRepeatDays") - .whereRaw(`"v2"."reminderRepeatDays" > 0`) + .whereRaw(`v2."reminderRepeatDays" > 0`) .groupBy("v2.secretId"); }) // Add LEFT JOIN with Reminder table to check for existing reminders @@ -56,7 +58,7 @@ export async function up(knex: Knex): Promise { }); }); - const commitBatches = chunkArray(reminderInserts, 9000); + const commitBatches = chunkArray(reminderInserts, 2000); for (const commitBatch of commitBatches) { const insertedReminders = (await knex .batchInsert(TableName.Reminder, commitBatch) @@ -79,7 +81,7 @@ export async function up(knex: Knex): Promise { userId: recipient.userId })); - const filteredRecipients = reminderRecipients.filter((recipient) => !!recipient.reminderId); + const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId)); await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients); } logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`); From 7ce11cde9569c5c9dd3ac10144671d0c30c17dee Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 14:47:57 -0300 Subject: [PATCH 4/5] Add cycle logic to next reminder migration --- ...250725144940_fix-secret-reminders-migration.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts index 9a0394bf9..b720c97ae 100644 --- a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts +++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts @@ -47,9 +47,22 @@ export async function up(knex: Knex): Promise { if (secretsWithLatestVersions.length > 0) { secretsWithLatestVersions.forEach((secret) => { if (!secret.reminderRepeatDays) return; - const nextReminderDate = new Date(secret.createdAt); + + const now = new Date(); + const createdAt = new Date(secret.createdAt); + let nextReminderDate = new Date(createdAt); nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays); + // If the next reminder date is in the past, calculate the proper next occurrence + if (nextReminderDate < now) { + const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)); + const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays; + const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle; + + nextReminderDate = new Date(now); + nextReminderDate.setDate(now.getDate() + daysUntilNextReminder); + } + reminderInserts.push({ secretId: secret.secretId, message: secret.reminderNote, From 72ee468208f8d1bf90c4c9020a7e1aa1037123df Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 25 Jul 2025 15:20:23 -0300 Subject: [PATCH 5/5] Remove previous queue running the migration --- backend/src/services/reminder/reminder-queue.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts index 1487a63f3..c038734bd 100644 --- a/backend/src/services/reminder/reminder-queue.ts +++ b/backend/src/services/reminder/reminder-queue.ts @@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({ { pattern: "0 */1 * * *", utc: true }, QueueName.SecretReminderMigration // just a job id ); - - await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, { - delay: 5000, - jobId: QueueName.SecretReminderMigration, - repeat: { pattern: "0 */1 * * *", utc: true } - }); }; queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {