afridho-mongodb
Version:
A simple MongoDB client wrapper for easy database operations
885 lines (884 loc) • 33.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObjectId = void 0;
const mongodb_1 = require("mongodb");
Object.defineProperty(exports, "ObjectId", { enumerable: true, get: function () { return mongodb_1.ObjectId; } });
const dotenv_1 = __importDefault(require("dotenv"));
const date_fns_1 = require("date-fns");
// Load environment variables
dotenv_1.default.config();
// Define MongoDB client options
const mongoOptions = {
connectTimeoutMS: 30000,
maxPoolSize: 20, // prevent excessive parallel connections
maxIdleTimeMS: 60000,
};
// Lazy getters for env vars — only validated when actually needed (not at import time).
// This allows the module to be imported during build without throwing.
function getDbUri() {
const uri = process.env.MONGODB_URI || "";
if (!uri) {
throw new Error("MONGODB_URI must be set in the environment");
}
return uri;
}
function getDbName() {
const name = process.env.DB_NAME || "";
if (!name) {
throw new Error("DB_NAME must be set in the environment");
}
return name;
}
async function getMongoClient() {
if (global._mongoClient)
return global._mongoClient;
console.log("🌱 Connecting to MongoDB...");
const client = new mongodb_1.MongoClient(getDbUri(), mongoOptions);
await client.connect();
global._mongoClient = client;
return client;
}
/**
* Class representing a MongoDB client for a specific collection.
*/
class ClientDB {
/**
* Returns the native MongoDB Db instance from the global connection.
* Useful for libraries that require a direct Db object (e.g. Better-Auth).
*/
static async getNativeDb() {
const client = await getMongoClient();
return client.db(getDbName());
}
/**
* Returns the native MongoDB Db instance for this instance's connection.
*/
async getNativeDb() {
await this.connect();
return this.client.db(getDbName());
}
/**
* Returns the native MongoDB Db instance synchronously.
* The driver will handle connection queuing in the background.
*/
static getNativeDbSync() {
if (!global._mongoClient) {
global._mongoClient = new mongodb_1.MongoClient(getDbUri(), mongoOptions);
}
return global._mongoClient.db(getDbName());
}
/**
* Creates an instance of ClientDB.
* @param {string} collectionName - The name of the collection to interact with.
*/
constructor(collectionName) {
// Use the validated DB_URI and predefined options
this.collectionName = collectionName;
this.collection = null;
}
/**
* Connects to the MongoDB database and initializes the collection.
* @returns {Promise<void>}
*/
async connect() {
if (!this.collection) {
this.client = await getMongoClient();
const db = this.client.db(getDbName());
this.collection = db.collection(this.collectionName);
}
}
/**
* Helper to convert _id string to ObjectId if needed
* @param {Document} query - The query object to preprocess
* @returns {Document} The processed query with _id converted if applicable
*/
preprocessQuery(query) {
if (query._id && typeof query._id === "string") {
try {
query._id = new mongodb_1.ObjectId(query._id);
}
catch (_a) {
// Invalid ObjectId string, leave as is or handle error if you prefer
}
}
return query;
}
/**
* Reads a document from the collection based on the provided query.
* @param {Document} query - The query to find the document.
* @returns {Promise<Document|null>} The found document or null if not found.
*/
async read(query) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.findOne(processedQuery);
}
/**
* Reads all documents from the collection with optional sorting.
* @param {ReadAllOptions} [options] - Optional options object.
* @param {Document} [options.sort] - Optional sort object, e.g. { createdAt: 1 } for ascending.
* @returns {Promise<Document[]>} An array of all documents, optionally sorted.
*/
async readAll(options) {
await this.connect();
let cursor = this.collection.find();
if (options === null || options === void 0 ? void 0 : options.sort) {
cursor = cursor.sort(options.sort);
}
if (options === null || options === void 0 ? void 0 : options.limit) {
cursor = cursor.limit(options.limit);
}
if (options === null || options === void 0 ? void 0 : options.skip) {
cursor = cursor.skip(options.skip);
}
return await cursor.toArray();
}
/**
* Inserts a new document into the collection.
* @param {Document} data - The data to insert.
* @returns {Promise<Document>} The result of the insert operation.
*/
async insert(data) {
await this.connect();
return await this.collection.insertOne(data);
}
/**
* Inserts multiple documents into the collection.
* @param {Document[]} data - The data to insert.
* @returns {Promise<Document>} The result of the insert operation.
*/
async insertMany(data) {
await this.connect();
return await this.collection.insertMany(data);
}
/**
* Updates a document in the collection based on the provided query.
* @param {Document} query - The query to find the document to update.
* @param {Document} data - The data to update.
* @returns {Promise<Document>} The result of the update operation.
*/
async update(query, data) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.updateOne(processedQuery, { $set: data });
}
/**
* Updates multiple documents in the collection.
* - If you pass Mongo operators ($set, $inc, etc), it will use them directly.
* - If you pass a plain object, it will wrap it inside $set.
*
* @param {Document} query - The filter to match documents.
* @param {Document} data - The update data (plain object or with operators).
* @returns {Promise<Document>} The result of the update operation.
*/
async updateMany(query, data, options = {}) {
var _a;
await this.connect();
const processedQuery = this.preprocessQuery(query);
const hasOperator = !Array.isArray(data) &&
Object.keys(data).some((key) => key.startsWith("$"));
const updateDoc = Array.isArray(data)
? data
: hasOperator
? data
: { $set: data };
return await this.collection.updateMany(processedQuery, updateDoc, {
upsert: (_a = options.upsert) !== null && _a !== void 0 ? _a : false,
});
}
/**
* Deletes a document from the collection based on the provided query.
* @param {Document} query - The query to find the document to delete.
* @returns {Promise<Document>} The result of the delete operation.
*/
async delete(query) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.deleteOne(processedQuery);
}
/**
* Deletes multiple documents from the collection based on the provided query.
* @param {Document} query - The query to find the documents to delete.
* @returns {Promise<Document>} The result of the delete operation.
*/
async deleteMany(query) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.deleteMany(processedQuery);
}
/**
* Finds multiple documents in the collection based on the provided query.
* @param {Document} query - The query to find the documents.
* @returns {Promise<Document[]>} An array of found documents.
*/
async find(query, options, project) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
let cursor = this.collection.find(processedQuery, project ? { projection: project } : undefined);
if (options === null || options === void 0 ? void 0 : options.sort) {
cursor = cursor.sort(options.sort);
}
if (options === null || options === void 0 ? void 0 : options.limit) {
cursor = cursor.limit(options.limit);
}
if (options === null || options === void 0 ? void 0 : options.skip) {
cursor = cursor.skip(options.skip);
}
return await cursor.toArray();
}
/**
* Reads multiple documents from the collection based on a query.
* @param {Document} query - The filter query.
* @returns {Promise<Document[]>} Array of matching documents.
*/
async readMany(query) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.find(processedQuery).toArray();
}
/**
* Gets random documents from the collection.
* @param {number} [total=1] - The number of random documents to retrieve.
* @returns {Promise<Document[]>} An array of random documents.
*/
async getRandomData(total = 1) {
await this.connect();
const pipeline = [{ $sample: { size: total } }];
return await this.collection.aggregate(pipeline).toArray();
}
/**
* Run an aggregation pipeline on the current collection.
*
* @param {Array<Document>} pipeline - An array of MongoDB aggregation stages.
* Example:
* [
* { $unwind: "$tags" },
* { $group: { _id: "$tags", count: { $sum: 1 } } },
* { $sort: { count: -1 } },
* { $limit: 10 }
* ]
*
* @returns {Promise<Document[]>} Resolves with the array of aggregation results.
*
* @throws {Error} If the aggregation query fails.
*/
async aggregate(pipeline = []) {
try {
await this.connect(); // ensure connected
return await this.collection.aggregate(pipeline).toArray();
}
catch (err) {
console.error("Aggregate error:", err);
throw err;
}
}
/**
* Gets the storage statistics for the collection.
* @returns {Promise<{storageSize: number, size: number, count: number}>} The storage statistics including storageSize.
*/
async getStorageStats() {
await this.connect();
const stats = await this.client
.db(getDbName())
.command({ collStats: this.collectionName });
return {
storageSize: stats.storageSize,
size: stats.size,
count: stats.count,
};
}
/**
* Gets cluster-wide storage statistics.
*/
static async getClusterStats() {
var _a;
const client = await getMongoClient();
const admin = client.db().admin();
const dbList = await admin.listDatabases();
let totalDataSize = 0;
let totalStorageSize = 0;
let totalIndexSize = 0;
const databases = [];
for (const dbInfo of dbList.databases) {
// Skip system databases that often have restricted access
if (["admin", "local", "config"].includes(dbInfo.name)) {
continue;
}
const db = client.db(dbInfo.name);
try {
const stats = await db.command({ dbStats: 1 });
totalDataSize += stats.dataSize || 0;
totalStorageSize += stats.storageSize || 0;
totalIndexSize += stats.indexSize || 0;
databases.push({
name: dbInfo.name,
dataSize: stats.dataSize,
storageSize: stats.storageSize,
indexSize: stats.indexSize,
collections: stats.collections,
objects: stats.objects,
});
}
catch (err) {
// Only log if it's not an authorization error to keep logs clean
if (!((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes("Unauthorized"))) {
console.error(`Error getting stats for db ${dbInfo.name}:`, err);
}
databases.push({
name: dbInfo.name,
error: err.message,
});
}
}
return {
totalDataSize,
totalStorageSize,
totalIndexSize,
databases,
};
}
/**
* Closes the MongoDB connection.
* @returns {Promise<void>}
*/
async close() {
await this.client.close();
}
/**
* Finds documents in the current collection and dynamically joins related collections
* using MongoDB's `$lookup` aggregation stage.
*
* This is useful when you want to "populate" data from other collections
* (similar to Mongoose's populate) but in a flexible, dynamic way.
*
* ### Example:
* ```ts
* const tasks = await tasksDb.findWithRelations(
* { column: "todo" }, // filter
* [
* {
* from: "kanban_tags",
* localField: "tags",
* foreignField: "value",
* as: "tags"
* },
* {
* from: "kanban_persons",
* localField: "persons",
* foreignField: "_id",
* as: "persons",
* isObjectId: true // convert string IDs to ObjectId
* }
* ],
* {
* title: 1,
* description: 1,
* "tags.label": 1,
* "persons.name": 1
* },
* {
* sort: { createdAt: -1 },
* limit: 10,
* skip: 20
* }
* );
* ```
*
* @param {Document} [filter={}] - MongoDB query filter. Defaults to `{}` (fetch all).
* @param {Object[]} [relations=[]] - Array of relation configurations for `$lookup`.
* @param {string} relations[].from - Target collection name to join with.
* @param {string} relations[].localField - Field in this collection that holds the reference.
* @param {string} relations[].foreignField - Field in the target collection to match against.
* @param {string} relations[].as - The alias name for the joined data in the output.
* @param {boolean} [relations[].isObjectId] - If `true`, will map string IDs in `localField` into ObjectId before lookup.
* @param {Document} [project={}] - Optional MongoDB projection object to limit fields in the final output.
* @param {ReadAllOptions} [options={}] - Optional settings like sort, skip, and limit.
*
* @returns {Promise<Document[]>} A promise that resolves to an array of documents with joined relations applied.
*/
async findWithRelations(filter = {}, relations = [], project = {}, options) {
await this.connect();
const processedQuery = this.preprocessQuery(filter);
const pipeline = [];
if (Object.keys(processedQuery).length > 0) {
pipeline.push({ $match: processedQuery });
}
for (const rel of relations) {
if (rel.isObjectId) {
if (rel.isSingle) {
// 👇 Single string → ObjectId
pipeline.push({
$addFields: {
[rel.localField]: {
$toObjectId: `$${rel.localField}`,
},
},
});
}
else {
// 👇 Array of strings → map to ObjectIds
pipeline.push({
$addFields: {
[rel.localField]: {
$map: {
input: `$${rel.localField}`,
as: "id",
in: { $toObjectId: "$$id" },
},
},
},
});
}
}
pipeline.push({
$lookup: {
from: rel.from,
localField: rel.localField,
foreignField: rel.foreignField,
as: rel.as,
},
});
}
if (options === null || options === void 0 ? void 0 : options.sort)
pipeline.push({ $sort: options.sort });
if (options === null || options === void 0 ? void 0 : options.skip)
pipeline.push({ $skip: options.skip });
if (options === null || options === void 0 ? void 0 : options.limit)
pipeline.push({ $limit: options.limit });
if (Object.keys(project).length > 0) {
pipeline.push({ $project: project });
}
return await this.collection.aggregate(pipeline).toArray();
}
/**
* Finds a single document in the current collection and dynamically joins related collections
* using MongoDB's `$lookup` aggregation stage.
*
* Mirip dengan `findWithRelations`, tapi hanya return satu dokumen (bukan array).
*
* ### Example:
* ```ts
* const task = await tasksDb.findOneWithRelations(
* { _id: "66cfa89f3c9c7d776b5f4f10" },
* [
* {
* from: "kanban_tags",
* localField: "tags",
* foreignField: "value",
* as: "tags"
* },
* {
* from: "kanban_persons",
* localField: "persons",
* foreignField: "_id",
* as: "persons",
* isObjectId: true
* }
* ]
* );
* ```
*
* @param {Document} filter - MongoDB query filter. Biasanya pakai `_id`.
* @param {Object[]} [relations=[]] - Array of relation configs sama seperti `findWithRelations`.
* @param {Document} [project={}] - Projection untuk limit field output.
*
* @returns {Promise<Document | null>} A single document with joined relations, or `null`.
*/
async findOneWithRelations(filter, relations = [], project = {}) {
await this.connect();
const processedQuery = this.preprocessQuery(filter);
const pipeline = [{ $match: processedQuery }];
// relations
for (const rel of relations) {
if (rel.isObjectId) {
if (rel.isSingle) {
pipeline.push({
$addFields: {
[rel.localField]: {
$toObjectId: `$${rel.localField}`,
},
},
});
}
else {
pipeline.push({
$addFields: {
[rel.localField]: {
$map: {
input: `$${rel.localField}`,
as: "id",
in: { $toObjectId: "$$id" },
},
},
},
});
}
}
pipeline.push({
$lookup: {
from: rel.from,
localField: rel.localField,
foreignField: rel.foreignField,
as: rel.as,
},
});
}
if (Object.keys(project).length > 0) {
pipeline.push({ $project: project });
}
const results = await this.collection.aggregate(pipeline).toArray();
return results[0] || null;
}
/**
* Counts the number of documents matching the query.
* Alias for countDocuments.
* @param {Document} [query={}] - Optional filter query.
* @returns {Promise<number>} The count of matching documents.
*/
async count(query = {}) {
return this.countDocuments(query);
}
/**
* Counts the number of documents matching the query.
* @param {Document} [query={}] - Optional filter query.
* @returns {Promise<number>} The count of matching documents.
*/
async countDocuments(query = {}) {
await this.connect();
const processedQuery = this.preprocessQuery(query);
return await this.collection.countDocuments(processedQuery);
}
/**
* One-time migration: convert string date fields to Date objects.
*
* @param fields - Which fields to convert (default: createdAt, updatedAt, startAt, endAt)
* @returns number of documents updated
*/
async migrateDateFields(fields = ["createdAt", "updatedAt", "startAt", "endAt"]) {
await this.connect();
const cursor = this.collection.find({
$or: fields.map((f) => ({ [f]: { $type: "string" } })),
});
let count = 0;
while (await cursor.hasNext()) {
const doc = await cursor.next();
if (!doc)
continue; // TS happy + runtime safe
const updates = {};
for (const field of fields) {
if (typeof doc[field] === "string") {
updates[field] = new Date(doc[field]);
}
}
if (Object.keys(updates).length > 0) {
await this.collection.updateOne({ _id: doc._id }, { $set: updates });
count++;
}
}
return count;
}
/**
* INTERNAL: Connect to external cluster
*/
static async _connectExternal(uri) {
const { MongoClient } = require("mongodb");
const client = new MongoClient(uri, {
maxPoolSize: 20,
connectTimeoutMS: 30000,
});
await client.connect();
return client;
}
/**
* INCREMENTAL BACKUP (NO DELETE):
* - Hanya ambil dokumen yang updatedAt > lastBackupAt
* - Tidak pernah hapus dokumen di backup
* - Upsert-only
*/
static async incrementalBackupOneDatabase(params) {
const { targetUri, sourceDb, targetDb } = params;
const sourceClient = await ClientDB._connectExternal(process.env.MONGODB_URI);
const targetClient = await ClientDB._connectExternal(targetUri);
const src = sourceClient.db(sourceDb);
const tgt = targetClient.db(targetDb);
const metaCol = tgt.collection("_backup_meta");
const meta = await metaCol.findOne({ _id: "incremental" });
const lastBackupAt = (meta === null || meta === void 0 ? void 0 : meta.lastBackupAt)
? new Date(meta.lastBackupAt)
: new Date(0);
const now = new Date();
const collections = await src.listCollections().toArray();
const report = [];
for (const c of collections) {
const name = c.name;
if (name === "_backup_meta")
continue;
const srcCol = src.collection(name);
const tgtCol = tgt.collection(name);
// ambil dokumen yang berubah
const changedDocs = await srcCol
.find({ updatedAt: { $gt: lastBackupAt } })
.toArray();
for (const doc of changedDocs) {
const { _id, ...data } = doc;
await tgtCol.updateOne({ _id: _id }, { $set: data }, { upsert: true });
}
report.push({
collection: name,
upserted: changedDocs.length,
});
}
// update checkpoint
await metaCol.updateOne({ _id: "incremental" }, { $set: { lastBackupAt: now } }, { upsert: true });
await sourceClient.close();
await targetClient.close();
return {
mode: "incremental",
sourceDb,
targetDb,
lastBackupAt,
executedAt: now,
report,
};
}
/**
* MULTI-DATABASE INCREMENTAL (NO DELETE)
*/
static async incrementalBackupManyDatabases(targetUri, dbList) {
const allResults = [];
for (const dbName of dbList) {
const r = await ClientDB.incrementalBackupOneDatabase({
targetUri,
sourceDb: dbName,
targetDb: dbName,
});
allResults.push(r);
}
return {
mode: "incremental",
targetUri,
results: allResults,
};
}
/**
* DELTA BACKUP (NO DELETE):
* - hanya dokumen baru (yang belum ada _id nya di backup)
* - tidak pernah hapus
*/
static async deltaBackupOneDatabase(params) {
const { targetUri, sourceDb, targetDb } = params;
const sourceClient = await ClientDB._connectExternal(process.env.MONGODB_URI);
const targetClient = await ClientDB._connectExternal(targetUri);
const src = sourceClient.db(sourceDb);
const tgt = targetClient.db(targetDb);
const collections = await src.listCollections().toArray();
const report = [];
for (const c of collections) {
const name = c.name;
const srcCol = src.collection(name);
const tgtCol = tgt.collection(name);
// semua _id di backup
const tgtDocs = await tgtCol
.find({}, { projection: { _id: 1 } })
.toArray();
const tgtIds = tgtDocs.map((d) => d._id);
// dokumen yang belum ada
const newDocs = await srcCol
.find({
_id: { $nin: tgtIds },
})
.toArray();
if (newDocs.length) {
await tgtCol.insertMany(newDocs);
}
report.push({
collection: name,
inserted: newDocs.length,
});
}
await sourceClient.close();
await targetClient.close();
return {
mode: "delta",
sourceDb,
targetDb,
report,
};
}
/**
* MULTI-DATABASE DELTA (NO DELETE)
*/
static async deltaBackupManyDatabases(targetUri, dbList) {
const allResults = [];
for (const dbName of dbList) {
const r = await ClientDB.deltaBackupOneDatabase({
targetUri,
sourceDb: dbName,
targetDb: dbName,
});
allResults.push(r);
}
return {
mode: "delta",
targetUri,
results: allResults,
};
}
static async fullSyncOneDatabase(params) {
const { sourceUri, targetUri, dbName } = params;
const now = new Date();
const week = (0, date_fns_1.getISOWeek)(now);
const year = (0, date_fns_1.getISOWeekYear)(now);
const snapshotDbName = `${dbName}-week-${week}-${year}`;
const srcClient = await ClientDB._connectExternal(sourceUri);
const tgtClient = await ClientDB._connectExternal(targetUri);
const src = srcClient.db(dbName);
const tgt = tgtClient.db(snapshotDbName);
const collections = await src.listCollections().toArray();
const report = [];
let totalInserted = 0;
let totalUpdated = 0;
let totalDeleted = 0;
for (const col of collections) {
const name = col.name;
const srcCol = src.collection(name);
const tgtCol = tgt.collection(name);
// --- Load all docs (lean, no heavy fields)
const srcDocs = await srcCol.find({}).toArray();
const tgtDocs = await tgtCol
.find({}, { projection: { _id: 1, updatedAt: 1 } })
.toArray();
const srcMap = new Map();
const tgtMap = new Map();
srcDocs.forEach((d) => srcMap.set(String(d._id), d));
tgtDocs.forEach((d) => tgtMap.set(String(d._id), d));
const inserts = [];
const updates = [];
const deletes = [];
// --- Compute inserts & updates
for (const [id, srcDoc] of srcMap.entries()) {
const tgtDoc = tgtMap.get(id);
if (!tgtDoc) {
inserts.push(srcDoc);
}
else {
// If requires update
const srcUpdatedAt = srcDoc.updatedAt instanceof Date
? srcDoc.updatedAt
: new Date(srcDoc.updatedAt);
const tgtUpdatedAt = tgtDoc.updatedAt instanceof Date
? tgtDoc.updatedAt
: new Date(tgtDoc.updatedAt);
if (srcUpdatedAt > tgtUpdatedAt) {
updates.push(srcDoc);
}
}
}
// --- Compute deletes
for (const [id, tgtDoc] of tgtMap.entries()) {
if (!srcMap.has(id)) {
deletes.push(tgtDoc._id);
}
}
// --- Apply inserts
if (inserts.length > 0) {
await tgtCol.insertMany(inserts);
}
// --- Apply updates (bulkWrite)
if (updates.length > 0) {
const bulkOps = updates.map((doc) => {
const { _id, ...data } = doc;
return {
updateOne: {
filter: { _id },
update: { $set: data },
},
};
});
await tgtCol.bulkWrite(bulkOps, { ordered: false });
}
// --- Apply deletes
if (deletes.length > 0) {
await tgtCol.deleteMany({
_id: { $in: deletes },
});
}
report.push({
collection: name,
inserted: inserts.length,
updated: updates.length,
deleted: deletes.length,
});
totalInserted += inserts.length;
totalUpdated += updates.length;
totalDeleted += deletes.length;
}
await srcClient.close();
await tgtClient.close();
return {
mode: "full-sync-diff",
sourceDb: dbName,
snapshotDb: snapshotDbName,
inserted: totalInserted,
updated: totalUpdated,
deleted: totalDeleted,
report,
};
}
static async fullSyncManyDatabases(params) {
var _a;
const { targetURI, dbs } = params;
const keepWeeks = (_a = params.keepWeeks) !== null && _a !== void 0 ? _a : 26;
const sourceUri = process.env.MONGODB_URI;
const tasks = dbs.map((dbName) => ClientDB.fullSyncOneDatabase({
sourceUri,
targetUri: targetURI,
dbName,
}));
// ⭐ Parallel execution
const results = await Promise.allSettled(tasks);
// Run cleanup AFTER all DBs synced
await ClientDB.cleanupSnapshots({
targetURI,
dbs,
keepWeeks,
});
return {
mode: "full-sync-diff",
syncedDatabases: dbs,
results,
};
}
static async cleanupSnapshots(params) {
var _a;
const { targetURI, dbs } = params;
const keepWeeks = (_a = params.keepWeeks) !== null && _a !== void 0 ? _a : 26;
const client = await ClientDB._connectExternal(targetURI);
const admin = client.db().admin();
const all = await admin.listDatabases();
const names = all.databases.map((d) => d.name);
const cutoff = (0, date_fns_1.subWeeks)(new Date(), keepWeeks);
const cutoffWeek = (0, date_fns_1.getISOWeek)(cutoff);
const cutoffYear = (0, date_fns_1.getISOWeekYear)(cutoff);
for (const base of dbs) {
const prefix = `${base}-week-`;
const matches = names.filter((name) => name.startsWith(prefix));
for (const dbName of matches) {
const parts = dbName.replace(prefix, "").split("-");
const week = Number(parts[0]);
const year = Number(parts[1]);
const isOld = year < cutoffYear ||
(year === cutoffYear && week < cutoffWeek);
if (isOld) {
console.log(`🗑 Removing old snapshot: ${dbName}`);
await client.db(dbName).dropDatabase();
}
}
}
await client.close();
}
}
exports.default = ClientDB;