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
4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"start": "node server.js",
"dev": "nodemon server.js",
"client": "npm run start --prefix client",
"heroku-postbuild": "cd client && npm install && npm run build"
"heroku-postbuild": "cd client && npm install && npm run build",
"clone-or-sync-projects": "node ./scripts/cloneOrSyncCollections.js",
"clear-dev-collections": "node ./scripts/clearDevCollections.js"
},
"author": "sarL3y",
"license": "ISC",
Expand Down
112 changes: 112 additions & 0 deletions backend/scripts/clearDevCollections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Script to clear the 'projects' and/or 'recurringevents' collections from the DEV MongoDB database.
*
* Usage:
* node clearDevCollections.js [--projects] [--recurring-events]
*
* Arguments:
* --projects Only clear the 'projects' collection
* --recurring-events Only clear the 'recurringevents' collection
* --events Only clear the 'events' collection
* --checkins Only clear the 'checkins' collection
* --all Clear all supported collections
* --mock Only print what would be deleted, do not delete
* --help Show this help message and exit
* (no flags) Show help (safety mode)
*
* Requires environment variable:
* DEV_DB_URI - MongoDB URI for DEV
*
* Example:
* node clearDevCollections.js --projects
* node clearDevCollections.js --recurring-events
* node clearDevCollections.js
*/

const { MongoClient } = require('mongodb');
require('dotenv').config();

const DEV_DB_NAME = 'vrms-test'; // Match the DEV_DB_NAME from cloneOrSyncProjects.js
const COLLECTIONS = {
projects: 'projects',
recurring_events: 'recurringevents',
events: 'events',
checkins: 'checkins',
};

function printHelp() {
console.log(
`\nUsage: node clearDevCollections.js [options]\n\nOptions:\n --projects Only clear the 'projects' collection\n --recurring-events Only clear the 'recurringevents' collection\n --events Only clear the 'events' collection\n --checkins Only clear the 'checkins' collection\n --all Clear all supported collections\n --mock Only print what would be deleted, do not delete\n --help Show this help message and exit\n\nExamples:\n node clearDevCollections.js --projects\n node clearDevCollections.js --recurring-events\n node clearDevCollections.js --all\n node clearDevCollections.js --mock --all\n`,
);
}

function checkEnv() {
if (!process.env.DEV_DB_URI) {
throw new Error('DEV_DB_URI environment variable must be set.');
}
}

async function clearCollection(devClient, collectionName, isMock) {
const devDb = devClient.db(DEV_DB_NAME);
if (isMock) {
const count = await devDb.collection(collectionName).countDocuments();
console.log(
`[MOCK] Would delete ${count} documents from ${collectionName} in DEV[${DEV_DB_NAME}].`,
);
} else {
const result = await devDb.collection(collectionName).deleteMany({});
console.log(
`[INFO] Cleared ${result.deletedCount} documents from ${collectionName} in DEV[${DEV_DB_NAME}].`,
);
}
}

async function main() {
checkEnv();
const doProjects = process.argv.includes('--projects');
const doRecurring = process.argv.includes('--recurring-events');
const doEvent = process.argv.includes('--events');
const doCheckIn = process.argv.includes('--checkins');
const doAll = process.argv.includes('--all');
const isMock = process.argv.includes('--mock');
const isHelp = process.argv.includes('--help');
const noArgs = process.argv.length <= 2;
if (isHelp || noArgs) {
printHelp();
return;
}
const collectionsToClear = [];
if (doAll) {
collectionsToClear.push('projects', 'recurring_events', 'events', 'checkins');
} else {
if (doProjects) collectionsToClear.push('projects');
if (doRecurring) collectionsToClear.push('recurring_events');
if (doEvent) collectionsToClear.push('events');
if (doCheckIn) collectionsToClear.push('checkins');
}
if (collectionsToClear.length === 0) {
printHelp();
return;
}

let devClient;
try {
devClient = new MongoClient(process.env.DEV_DB_URI);
await devClient.connect();
for (const key of collectionsToClear) {
await clearCollection(devClient, COLLECTIONS[key], isMock);
}
if (!isMock) {
console.log('[SUCCESS] Collections cleared.');
}
} catch (err) {
console.error('[ERROR]', err.message);
process.exitCode = 1;
} finally {
if (devClient) await devClient.close();
}
}

if (require.main === module) {
main();
}
217 changes: 217 additions & 0 deletions backend/scripts/cloneOrSyncCollections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Script to clone or sync collections from PROD to DEV MongoDB.
*
* Usage:
* node cloneOrSyncProjects.js [options]
*
* Options:
* --projects Clone/sync the 'projects' collection
* --recurring-events Clone/sync the 'recurringevents' collection
* --all Clone/sync all supported collections
* --initial Initial clone (insertMany, skip duplicates)
* --mock Only print what would be written, do not write
* --help Show this help message and exit
* (no flags) Show help (safety mode)
*
* Requires environment variables:
* PROD_DB_URI - MongoDB URI for PROD
* DEV_DB_URI - MongoDB URI for DEV
*
* Examples:
* node cloneOrSyncProjects.js --projects
* node cloneOrSyncProjects.js --recurring-events --initial
* node cloneOrSyncProjects.js --all --mock
*/

const { MongoClient } = require('mongodb');
const mongoose = require('mongoose');
require('dotenv').config();

// Print help message for CLI usage
function printHelp() {
console.log(
`\nUsage: node cloneOrSyncProjects.js [options]\n\nOptions:\n --projects Clone/sync the 'projects' collection\n --recurring-events Clone/sync the 'recurringevents' collection\n --all Clone/sync all supported collections\n --initial Initial clone (insertMany, skip duplicates)\n --mock Only print what would be written, do not write\n --help Show this help message and exit\n\nExamples:\n node cloneOrSyncProjects.js --projects\n node cloneOrSyncProjects.js --recurring-events\n node cloneOrSyncProjects.js --all --mock\n`,
);
}

// ---- CONSTANTS ----
const COLLECTIONS = {
projects: {
name: 'projects',
model: 'Project',
},
recurring_events: {
name: 'recurringevents',
model: 'RecurringEvent',
},
};
const PROD_DB_NAME = 'db';
const DEV_DB_NAME = 'vrms-test';
// ---- CONSTANTS ----
// const COLLECTION_NAME = 'projects';
// const PROD_DB_NAME = 'vrms-test';
// const DEV_DB_NAME = 'vrms-test-sync';

/**
* Throws if required environment variables are missing.
*/
function checkEnv() {
if (!process.env.PROD_DB_URI || !process.env.DEV_DB_URI) {
throw new Error('Both PROD_DB_URI and DEV_DB_URI environment variables must be set.');
}
}

/**
* Connects to both PROD (via Mongoose) and DEV (via MongoClient) MongoDB databases.
* @returns {Promise<{prod: typeof mongoose, dev: MongoClient}>}
*/
async function connectDbs() {
// Connect PROD using Mongoose
await mongoose.connect(process.env.PROD_DB_URI, {
dbName: PROD_DB_NAME,
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Connect DEV using MongoClient
const dev = new MongoClient(process.env.DEV_DB_URI);
await dev.connect();
return { prod: mongoose, dev };
}

/**
* Fetches all projects from the PROD database using the Project Mongoose model.
* @returns {Promise<Array<Object>>}
*/

async function fetchProdCollection(collectionKey) {
const { Project, RecurringEvent } = require('../models');
let Model;
if (collectionKey === 'projects') Model = Project;
else if (collectionKey === 'recurring_events') Model = RecurringEvent;
else throw new Error('Unknown collection: ' + collectionKey);
const docs = await Model.find({});
console.log(
`[INFO] Fetched ${docs.length} ${COLLECTIONS[collectionKey].name} from PROD[${PROD_DB_NAME}].`,
);
return docs.map((doc) => doc.toObject());
}

/**
* Performs the initial clone: insertMany, skip duplicates.
* @param {MongoClient} devClient
* @param {Array<Object>} projects
* @returns {Promise<void>}
*/

async function initialClone(devClient, collectionKey, docs) {
const devDb = devClient.db(DEV_DB_NAME);
try {
const result = await devDb
.collection(COLLECTIONS[collectionKey].name)
.insertMany(docs, { ordered: false });
console.log(
`[INFO] Inserted ${result.insertedCount} ${COLLECTIONS[collectionKey].name} into DEV[${DEV_DB_NAME}].`,
);
} catch (err) {
if (err.code === 11000 || (err.writeErrors && err.writeErrors[0]?.code === 11000)) {
// Duplicate key error: some docs inserted, some skipped
const inserted = err.result?.result?.nInserted || err.result?.insertedCount || 0;
console.log(
`[WARN] Duplicate key error. Inserted ${inserted} new ${COLLECTIONS[collectionKey].name}, skipped duplicates.`,
);
} else {
throw err;
}
}
}

/**
* Syncs projects: upsert by _id using bulkWrite.
* @param {MongoClient} devClient
* @param {Array<Object>} projects
* @returns {Promise<void>}
*/

async function syncCollection(devClient, collectionKey, docs) {
const devDb = devClient.db(DEV_DB_NAME);
const ops = docs.map((doc) => ({
updateOne: {
filter: { _id: doc._id },
update: { $set: doc },
upsert: true,
},
}));
const result = await devDb
.collection(COLLECTIONS[collectionKey].name)
.bulkWrite(ops, { ordered: false });
console.log(
`[INFO] Upserted ${result.upsertedCount}, matched ${result.matchedCount}, modified ${result.modifiedCount} ${COLLECTIONS[collectionKey].name} in DEV.`,
);
}

/**
* Main coordinator function.
*/
async function main() {
checkEnv();
const isInitial = process.argv.includes('--initial');
const isMock = process.argv.includes('--mock');
const isHelp = process.argv.includes('--help');
const doProjects = process.argv.includes('--projects');
const doRecurring = process.argv.includes('--recurring-events');
const doAll = process.argv.includes('--all');
const noArgs = process.argv.length <= 2;
if (isHelp || noArgs) {
printHelp();
return;
}
const collectionsToClone = [];
if (doAll) {
collectionsToClone.push('projects', 'recurring_events');
} else {
if (doProjects) collectionsToClone.push('projects');
if (doRecurring) collectionsToClone.push('recurring_events');
}
if (collectionsToClone.length === 0) {
printHelp();
return;
}

let devClient;
try {
const dbs = await connectDbs();
devClient = dbs.dev;
for (const collectionKey of collectionsToClone) {
const docs = await fetchProdCollection(collectionKey);
if (isMock) {
if (isInitial) {
console.log(
`[MOCK] Would insert ${docs.length} ${COLLECTIONS[collectionKey].name} into DEV[${DEV_DB_NAME}].`,
);
} else {
console.log(
`[MOCK] Would upsert ${docs.length} ${COLLECTIONS[collectionKey].name} into DEV[${DEV_DB_NAME}].`,
);
}
} else {
if (isInitial) {
await initialClone(devClient, collectionKey, docs);
} else {
await syncCollection(devClient, collectionKey, docs);
}
}
}
if (!isMock) console.log('[SUCCESS] Operation completed.');
} catch (err) {
console.error('[ERROR]', err.message);
process.exitCode = 1;
} finally {
// Close mongoose connection
await mongoose.disconnect();
if (devClient) await devClient.close();
}
}

if (require.main === module) {
main();
}
Loading