diff --git a/cli.mjs b/cli.mjs index 82f8c900..b30b7ff8 100644 --- a/cli.mjs +++ b/cli.mjs @@ -1,8 +1,10 @@ import { checkbox, confirm, expand, input, select } from "@inquirer/prompts"; import fsExtra from "fs-extra"; -import { cert, initializeApp } from "firebase-admin/app"; // eslint-disable-line import/no-unresolved -import { getFirestore } from "firebase-admin/firestore"; // eslint-disable-line import/no-unresolved +// TODO @brown-ccv #183: Upgrade to modular SDK instead of compat +import { cert, initializeApp } from "firebase-admin/app"; +import { getFirestore } from "firebase-admin/firestore"; +import { Command } from "commander"; /** -------------------- GLOBALS -------------------- */ @@ -17,16 +19,73 @@ let OUTPUT_ROOT; // The root in which data is saved const INVALID_ACTION_ERROR = new Error("Invalid action: " + ACTION); const INVALID_DEPLOYMENT_ERROR = new Error("Invalid deployment: " + DEPLOYMENT); +/** -------------------- COMMANDER -------------------- */ +const commander = new Command(); +// default: [download | delete ] not provided, run main() as usual continuing with prompting +commander.action(() => {}); + +// download: optional argument studyID and participantID skips relative prompts +commander + .command(`download`) + .argument(`[studyID]`) + .argument(`[participantID]`) + .description(`Download experiment data from Firebase provided study ID and participant ID`) + .action((studyID, participantID) => { + ACTION = "download"; + STUDY_ID = studyID; + PARTICIPANT_ID = participantID; + }); + +// delete: optional argument studyID and participantID skips relative prompts +commander + .command(`delete`) + .argument(`[studyID]`) + .argument(`[participantID]`) + .description(`Delete experiment data from Firebase provided study ID and participant ID`) + .action((studyID, participantID) => { + ACTION = "delete"; + STUDY_ID = studyID; + PARTICIPANT_ID = participantID; + }); +commander.parse(); + +// print message if download or delete provided, along with optional args provided +if (ACTION !== undefined) { + console.log( + `${ACTION} data from Firebase given ${STUDY_ID === undefined ? "" : `study ID: ${STUDY_ID}`} ${PARTICIPANT_ID === undefined ? "" : `and participant ID: ${PARTICIPANT_ID}`}` + ); +} + /** -------------------- MAIN -------------------- */ // TODO @brown-ccv #289: Pass CLI arguments with commander (especially for action) async function main() { - ACTION = await actionPrompt(); + if (ACTION === undefined) { + ACTION = await actionPrompt(); + } DEPLOYMENT = await deploymentPrompt(); // TODO @brown-ccv #291: Enable downloading all study data at once - STUDY_ID = await studyIDPrompt(); + if (STUDY_ID === undefined) { + STUDY_ID = await studyIDPrompt(); + } else { + // when args directly passed in through CLI, check if study is valid + const studyCollection = await validateStudyFirebase(STUDY_ID); + if (!studyCollection) { + console.error("Please enter a valid study from your Firestore database"); + process.exit(1); + } + } // TODO @brown-ccv #291: Enable downloading all participant data at once - PARTICIPANT_ID = await participantIDPrompt(); + if (PARTICIPANT_ID === undefined) { + PARTICIPANT_ID = await participantIDPrompt(); + } else { + // when args directly passed in through CLI, check if participant is valid + const participantCollection = await validateParticipantFirebase(PARTICIPANT_ID); + if (!participantCollection) { + console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); + process.exit(1); + } + } EXPERIMENT_IDS = await experimentIDPrompt(); switch (ACTION) { @@ -54,7 +113,6 @@ async function main() { } } main(); - /** -------------------- DOWNLOAD ACTION -------------------- */ /** Download data that's stored in Firebase */ @@ -186,20 +244,14 @@ async function deploymentPrompt() { /** Prompt the user to enter the ID of a study */ async function studyIDPrompt() { const invalidMessage = "Please enter a valid study from your Firestore database"; - const validateStudyFirebase = async (input) => { - // subcollection is programmatically generated, if it doesn't exist then input must not be a valid studyID - const studyIDCollections = await getStudyRef(input).listCollections(); - return studyIDCollections.find((c) => c.id === PARTICIPANTS_COL) ? true : invalidMessage; - }; - return await input({ message: "Select a study:", validate: async (input) => { if (!input) return invalidMessage; - switch (DEPLOYMENT) { case "firebase": - return validateStudyFirebase(input); + const studyCollection = await validateStudyFirebase(input); + return studyCollection || invalidMessage; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -210,12 +262,6 @@ async function studyIDPrompt() { /** Prompt the user to enter the ID of a participant on the STUDY_ID study */ async function participantIDPrompt() { const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; - const validateParticipantFirebase = async (input) => { - // subcollection is programmatically generated, if it doesn't exist then input must not be a valid participantID - const studyIDCollections = await getParticipantRef(STUDY_ID, input).listCollections(); - return studyIDCollections.find((c) => c.id === DATA_COL) ? true : invalidMessage; - }; - return await input({ message: "Select a participant:", validate: async (input) => { @@ -225,7 +271,8 @@ async function participantIDPrompt() { switch (DEPLOYMENT) { case "firebase": - return validateParticipantFirebase(input); + const participantCollection = await validateParticipantFirebase(input); + return participantCollection || invalidMessage; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -312,6 +359,21 @@ async function confirmOverwritePrompt(file, overwriteAll) { return answer; } +/** -------------------- FIRESTORE VALIDATIONS -------------------- */ +/** helper to check if the given study (input) is in firestore */ +async function validateStudyFirebase(input) { + // subcollection is programmatically generated, if it doesn't exist then input must not be a valid studyID + const studyIDCollections = await getStudyRef(input).listCollections(); + return studyIDCollections.find((c) => c.id === PARTICIPANTS_COL); +} + +/** helper to check if the given participant (input) is in firestore under study */ +async function validateParticipantFirebase(input) { + // subcollection is programmatically generated, if it doesn't exist then input must not be a valid participantID + const studyIDCollections = await getParticipantRef(STUDY_ID, input).listCollections(); + return studyIDCollections.find((c) => c.id === DATA_COL); +} + /** -------------------- FIRESTORE HELPERS -------------------- */ const RESPONSES_COL = "participant_responses"; diff --git a/package-lock.json b/package-lock.json index 1981eae6..47f00e60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@jspsych/plugin-instructions": "^1.1.3", "@jspsych/plugin-preload": "^1.1.2", "@jspsych/plugin-survey": "^1.0.1", + "commander": "^12.1.0", "electron-log": "^5.0.0", "electron-squirrel-startup": "^1.0.0", "firebase": "^10.11.0", @@ -655,6 +656,15 @@ "node": ">= 16.4.0" } }, + "node_modules/@electron-forge/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@electron-forge/cli/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6306,13 +6316,11 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/commitizen": { @@ -9638,6 +9646,15 @@ "node": ">= 10" } }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/firebase-tools/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -12539,16 +12556,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/lint-staged/node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", diff --git a/package.json b/package.json index 83ea4663..7f79f107 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@jspsych/plugin-instructions": "^1.1.3", "@jspsych/plugin-preload": "^1.1.2", "@jspsych/plugin-survey": "^1.0.1", + "commander": "^12.1.0", "electron-log": "^5.0.0", "electron-squirrel-startup": "^1.0.0", "firebase": "^10.11.0",