|
| 1 | +import { onSchedule } from "firebase-functions/v2/scheduler"; |
| 2 | +import * as admin from "firebase-admin"; |
| 3 | +import * as logger from "firebase-functions/logger"; |
| 4 | +import { toError } from "../common/error"; |
| 5 | + |
| 6 | +const LOCATION = "asia-northeast3"; |
| 7 | +const CLEANUP_BATCH_SIZE = 200; |
| 8 | +const TOMBSTONE_GRACE_PERIOD_HOURS = 24; |
| 9 | + |
| 10 | +// 삭제 후 유예 기간이 지난 todo를 표시용 최소 필드만 남는 축약 문서 형태로 압축 |
| 11 | +export const compactSoftDeletedTodos = onSchedule({ |
| 12 | + maxInstances: 1, |
| 13 | + region: LOCATION, |
| 14 | + schedule: "0 9 * * *", |
| 15 | + timeZone: "Asia/Seoul" |
| 16 | + }, |
| 17 | + async () => { |
| 18 | + const cutoff = new Date(Date.now() - (TOMBSTONE_GRACE_PERIOD_HOURS * 60 * 60 * 1000)); |
| 19 | + |
| 20 | + try { |
| 21 | + let lastDocument: |
| 22 | + FirebaseFirestore.QueryDocumentSnapshot<FirebaseFirestore.DocumentData> | undefined; |
| 23 | + |
| 24 | + while (true) { |
| 25 | + let query = admin.firestore() |
| 26 | + .collectionGroup("todoLists") |
| 27 | + .where("compactedAt", "==", null) |
| 28 | + .where("deletedAt", "<=", admin.firestore.Timestamp.fromDate(cutoff)) |
| 29 | + .orderBy("deletedAt") |
| 30 | + .orderBy(admin.firestore.FieldPath.documentId()) |
| 31 | + .limit(CLEANUP_BATCH_SIZE); |
| 32 | + if (lastDocument) { |
| 33 | + query = query.startAfter(lastDocument); |
| 34 | + } |
| 35 | + |
| 36 | + const snapshot = await query.get(); |
| 37 | + if (snapshot.empty) { return; } |
| 38 | + |
| 39 | + const batch = admin.firestore().batch(); |
| 40 | + snapshot.docs.forEach((document) => { |
| 41 | + batch.update(document.ref, { |
| 42 | + compactedAt: admin.firestore.FieldValue.serverTimestamp(), |
| 43 | + content: admin.firestore.FieldValue.delete(), |
| 44 | + dueDate: admin.firestore.FieldValue.delete(), |
| 45 | + isChecked: admin.firestore.FieldValue.delete(), |
| 46 | + isCompleted: admin.firestore.FieldValue.delete(), |
| 47 | + isDeleting: admin.firestore.FieldValue.delete(), |
| 48 | + isPinned: admin.firestore.FieldValue.delete(), |
| 49 | + isDeleted: admin.firestore.FieldValue.delete(), |
| 50 | + tags: admin.firestore.FieldValue.delete() |
| 51 | + }); |
| 52 | + }); |
| 53 | + await batch.commit(); |
| 54 | + |
| 55 | + if (snapshot.size < CLEANUP_BATCH_SIZE) { return; } |
| 56 | + lastDocument = snapshot.docs[snapshot.docs.length - 1]; |
| 57 | + } |
| 58 | + } catch (error) { |
| 59 | + logger.error( |
| 60 | + "soft deleted todo 축약 문서 압축 실패", |
| 61 | + toError(error), |
| 62 | + { |
| 63 | + collectionGroup: "todoLists", |
| 64 | + filter: `compactedAt == null && deletedAt <= now - ${TOMBSTONE_GRACE_PERIOD_HOURS}h`, |
| 65 | + orderBy: ["deletedAt", "documentId"], |
| 66 | + cleanupBatchSize: CLEANUP_BATCH_SIZE |
| 67 | + } |
| 68 | + ); |
| 69 | + } |
| 70 | + } |
| 71 | +); |
0 commit comments