From 37fcb2976cebbebeb7ffadb3238cdcb8d8bd1e1f Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Fri, 26 Jul 2024 22:04:57 -0500 Subject: [PATCH 01/16] add: commander to optionally provide delete or download along with studyID and participantID to cmd line --- cli.mjs | 51 +++++++++++++++++++++++++++++++++++++++++++---- package-lock.json | 46 ++++++++++++++++++++++++++++-------------- package.json | 1 + 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/cli.mjs b/cli.mjs index 7323e14f9..2202d38da 100644 --- a/cli.mjs +++ b/cli.mjs @@ -4,6 +4,7 @@ import fsExtra from "fs-extra"; // 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 -------------------- */ @@ -18,16 +19,59 @@ 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(); + } // TODO @brown-ccv #291: Enable downloading all participant data at once - PARTICIPANT_ID = await participantIDPrompt(); + if (PARTICIPANT_ID == undefined) { + PARTICIPANT_ID = await participantIDPrompt(); + } EXPERIMENT_IDS = await experimentIDPrompt(); switch (ACTION) { @@ -55,7 +99,6 @@ async function main() { } } main(); - /** -------------------- DOWNLOAD ACTION -------------------- */ /** Download data that's stored in Firebase */ diff --git a/package-lock.json b/package-lock.json index 01bc5a134..c6c8a789f 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", "execa": "^8.0.1", @@ -2796,6 +2797,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", @@ -10645,12 +10655,11 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "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": { @@ -15046,6 +15055,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", @@ -20067,16 +20085,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", @@ -27845,6 +27853,14 @@ "balanced-match": "^1.0.0" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/sucrase/node_modules/glob": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", diff --git a/package.json b/package.json index 219595a94..7551244ef 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", "execa": "^8.0.1", From bbc569db50669aa8896f27405e5225e0284f410e Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Sat, 27 Jul 2024 17:30:36 -0500 Subject: [PATCH 02/16] add: register new participant under study in registered_studies for firestore --- cli.mjs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/cli.mjs b/cli.mjs index 2202d38da..ad77bcf0c 100644 --- a/cli.mjs +++ b/cli.mjs @@ -47,12 +47,25 @@ commander STUDY_ID = studyID; PARTICIPANT_ID = participantID; }); + +// register: optional argument studyID and participantID skips relative prompts +commander + .command(`register`) + .argument(`[studyID]`) + .argument(`[participantID]`) + .description(`Register new partipant under study provided a partipantID and studyID`) + .action((studyID, participantID) => { + ACTION = "register"; + 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}`}` + `${ACTION} data from Firebase ${STUDY_ID == undefined ? "" : `given study ID: ${STUDY_ID}`} ${PARTICIPANT_ID == undefined ? "" : `and participant ID: ${PARTICIPANT_ID}`}` ); } @@ -94,6 +107,15 @@ async function main() { throw INVALID_DEPLOYMENT_ERROR; } break; + case "register": + switch (DEPLOYMENT) { + case "firebase": + await registerDataFirebase(STUDY_ID, PARTICIPANT_ID); + break; + default: + throw INVALID_DEPLOYMENT_ERROR; + } + break; default: throw INVALID_ACTION_ERROR; } @@ -184,6 +206,24 @@ async function deleteDataFirebase() { } else console.log("Skipping deletion"); } +/** -------------------- REGISTER ACTION -------------------- */ + +/** Register new data, write to Firestore */ +async function registerDataFirebase(studyID, participantID) { + const confirmation = await confirmRegisterPrompt(studyID, participantID); + if (confirmation) { + try { + await addStudyAndParticipant(studyID, participantID); + } catch (error) { + console.error( + `Unable to register new participant with participantID ${participantID} under studyID ${studyID}: ` + + error + ); + } + } else console.log("Skipping registration"); + return true; +} + /** -------------------- PROMPTS -------------------- */ /** Prompt the user for the action they are trying to complete */ @@ -195,6 +235,10 @@ async function actionPrompt() { name: "Download data", value: "download", }, + { + name: "Register new participant under study", + value: "register", + }, { name: "Delete data", value: "delete", @@ -240,7 +284,10 @@ async function studyIDPrompt() { message: "Select a study:", validate: async (input) => { if (!input) return invalidMessage; - + if (ACTION == "register") { + STUDY_ID = input; + return true; + } switch (DEPLOYMENT) { case "firebase": return validateStudyFirebase(input); @@ -261,12 +308,15 @@ async function participantIDPrompt() { }; return await input({ - message: "Select a participant:", + message: ACTION == "register" ? "Enter a new participant:" : "Select a participant:", validate: async (input) => { const invalid = "Please enter a valid participant from your Firestore database"; if (!input) return invalid; else if (input === "*") return true; - + if (ACTION == "register") { + PARTICIPANT_ID = input; + return true; + } switch (DEPLOYMENT) { case "firebase": return validateParticipantFirebase(input); @@ -279,6 +329,11 @@ async function participantIDPrompt() { /** Prompt the user to select one or more experiments of the PARTICIPANT_ID on STUDY_ID */ async function experimentIDPrompt() { + // register: adding/checking for existing new studies will be done in function + if (ACTION == "register") { + return; + } + const dataSnapshot = await getDataRef(STUDY_ID, PARTICIPANT_ID).get(); // Sort experiment choices by most recent first @@ -322,6 +377,20 @@ async function confirmDeletionPrompt() { }); } +async function confirmRegisterPrompt(studyID, participantID) { + const currentParticipants = await getRegisteredParticipantArr(studyID); + const currentParticipantMessage = + currentParticipants.length == 0 + ? "Currently, there are no participants under this study\n" + : `Currently, the participants under this study include: \n${currentParticipants.join("\n")}\n`; + return confirm({ + message: + currentParticipantMessage + + `Continue? adding study with studyID: ${studyID} and participant ID: ${participantID}`, + default: false, + }); +} + /** * Prompts the user to confirm continuation of the CLI, including future conflicts * @param {string} outputFile @@ -359,6 +428,7 @@ async function confirmOverwritePrompt(file, overwriteAll) { /** -------------------- FIRESTORE HELPERS -------------------- */ const RESPONSES_COL = "participant_responses"; +const REG_STUDY_COL = "registered_studies"; const PARTICIPANTS_COL = "participants"; const DATA_COL = "data"; const TRIALS_COL = "trials"; @@ -377,3 +447,54 @@ const getDataRef = (studyID, participantID) => // Get a reference to a participant's specific experiment data document in Firestore const getExperimentRef = (studyID, participantID, experimentID) => getDataRef(studyID, participantID).doc(experimentID); + +// Get a reference to a registered study +const getRegisteredfStudyRef = (studyID) => FIRESTORE.collection(REG_STUDY_COL).doc(studyID); + +// Get current registered participant array under the StudyID +const getRegisteredParticipantArr = (studyID) => { + const res = getRegisteredfStudyRef(studyID) + .get() + .then((data) => { + if (data["_fieldsProto"] != undefined) { + return data["_fieldsProto"]["registered_participants"]["arrayValue"]["values"] + .filter((item) => item.valueType === "stringValue") + .map((item) => item.stringValue); + } else { + return []; + } + }); + + return res; +}; + +// Register new participantID under the provided studyID +const registerNewParticipant = async (studyID, participantID) => { + const currParticipants = await getRegisteredParticipantArr(studyID); + const newParticipantArray = [...currParticipants, participantID]; + const newData = { registered_participants: newParticipantArray }; + getRegisteredfStudyRef(studyID).update(newData); + console.log( + `Successfully added study and participant. Current participantIDs under study ${studyID}: \n${newParticipantArray.join("\n")}` + ); +}; + +// Add new participantID under studyID to Firestore under registered_studies, +// creates new study if studyID doesn't exist +const addStudyAndParticipant = async (studyID, participantID) => { + getRegisteredfStudyRef(studyID) + .get() + .then((data) => { + // study not initated yet + if (data["_fieldsProto"] == undefined) { + getRegisteredfStudyRef(studyID) + .set({ registered_participants: [] }) + .then(() => { + registerNewParticipant(studyID, participantID); + }); + } else { + // study initiated, add to array holding registered participants + registerNewParticipant(studyID, participantID); + } + }); +}; From aaf6969d688f4f0c62bb0dd1cf1c06a157e69f09 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Sat, 27 Jul 2024 17:55:14 -0500 Subject: [PATCH 03/16] ref: pulled out validateStudy and validateParticipant from prompting functions to use to check if arguments passed into CLI directly when running delete and download are valid --- cli.mjs | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/cli.mjs b/cli.mjs index 2202d38da..25e159c7b 100644 --- a/cli.mjs +++ b/cli.mjs @@ -67,10 +67,24 @@ async function main() { // TODO @brown-ccv #291: Enable downloading all study data at once if (STUDY_ID == undefined) { STUDY_ID = await studyIDPrompt(); + } else { + // when args directly passed in through CLI, check if study is valid + const hasStudy = await validateStudyFirebase(STUDY_ID); + if (hasStudy != true) { + console.error(hasStudy); + return; + } } // TODO @brown-ccv #291: Enable downloading all participant data at once if (PARTICIPANT_ID == undefined) { PARTICIPANT_ID = await participantIDPrompt(); + } else { + // when args directly passed in through CLI, check if participant is valid + const hasParticipant = await validateParticipantFirebase(STUDY_ID); + if (hasParticipant != true) { + console.error(hasParticipant); + return; + } } EXPERIMENT_IDS = await experimentIDPrompt(); @@ -228,19 +242,19 @@ async function deploymentPrompt() { } /** Prompt the user to enter the ID of a study */ -async function studyIDPrompt() { +// helper to check if the given study (input) is in firestore +const validateStudyFirebase = async (input) => { 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; - }; + // 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; +}; +async function studyIDPrompt() { return await input({ message: "Select a study:", validate: async (input) => { if (!input) return invalidMessage; - switch (DEPLOYMENT) { case "firebase": return validateStudyFirebase(input); @@ -252,14 +266,15 @@ async function studyIDPrompt() { } /** Prompt the user to enter the ID of a participant on the STUDY_ID study */ -async function participantIDPrompt() { +// helper to check if the given participant (input) is in firestore under study +const validateParticipantFirebase = async (input) => { 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; - }; + // 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; +}; +async function participantIDPrompt() { return await input({ message: "Select a participant:", validate: async (input) => { From ccaaf88759710fefcbe175490647dca7c2a83655 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Sat, 27 Jul 2024 18:03:29 -0500 Subject: [PATCH 04/16] bypass validateStudy and validateParticipant when action is register --- cli.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli.mjs b/cli.mjs index 427227520..a17d512da 100644 --- a/cli.mjs +++ b/cli.mjs @@ -83,7 +83,7 @@ async function main() { } else { // when args directly passed in through CLI, check if study is valid const hasStudy = await validateStudyFirebase(STUDY_ID); - if (hasStudy != true) { + if (hasStudy != true && ACTION != "register") { console.error(hasStudy); return; } @@ -94,7 +94,7 @@ async function main() { } else { // when args directly passed in through CLI, check if participant is valid const hasParticipant = await validateParticipantFirebase(STUDY_ID); - if (hasParticipant != true) { + if (hasParticipant != true && ACTION != "register") { console.error(hasParticipant); return; } From c2ec9eb48a3385bddcadd9a31a596d26134ba39f Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Thu, 1 Aug 2024 12:43:33 -0500 Subject: [PATCH 05/16] ref: function header, validate return values --- cli.mjs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/cli.mjs b/cli.mjs index 25e159c7b..2d6b6f41c 100644 --- a/cli.mjs +++ b/cli.mjs @@ -52,7 +52,7 @@ 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}`}` + `${ACTION} data from Firebase given ${STUDY_ID === undefined ? "" : `study ID: ${STUDY_ID}`} ${PARTICIPANT_ID === undefined ? "" : `and participant ID: ${PARTICIPANT_ID}`}` ); } @@ -70,8 +70,8 @@ async function main() { } else { // when args directly passed in through CLI, check if study is valid const hasStudy = await validateStudyFirebase(STUDY_ID); - if (hasStudy != true) { - console.error(hasStudy); + if (!hasStudy) { + console.error("Please enter a valid study from your Firestore database"); return; } } @@ -80,9 +80,9 @@ async function main() { PARTICIPANT_ID = await participantIDPrompt(); } else { // when args directly passed in through CLI, check if participant is valid - const hasParticipant = await validateParticipantFirebase(STUDY_ID); - if (hasParticipant != true) { - console.error(hasParticipant); + const hasParticipant = await validateParticipantFirebase(PARTICIPANT_ID); + if (!hasParticipant) { + console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); return; } } @@ -243,21 +243,22 @@ async function deploymentPrompt() { /** Prompt the user to enter the ID of a study */ // helper to check if the given study (input) is in firestore -const validateStudyFirebase = async (input) => { - const invalidMessage = "Please enter a valid study from your Firestore database"; +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) ? true : invalidMessage; -}; + return studyIDCollections.find((c) => c.id === PARTICIPANTS_COL); +} async function studyIDPrompt() { + const invalidMessage = "Please enter a valid study from your Firestore database"; return await input({ message: "Select a study:", validate: async (input) => { if (!input) return invalidMessage; switch (DEPLOYMENT) { case "firebase": - return validateStudyFirebase(input); + const res = await validateStudyFirebase(input); + return !res ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -267,14 +268,14 @@ async function studyIDPrompt() { /** Prompt the user to enter the ID of a participant on the STUDY_ID study */ // helper to check if the given participant (input) is in firestore under study -const validateParticipantFirebase = async (input) => { - const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; +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) ? true : invalidMessage; -}; + return studyIDCollections.find((c) => c.id === DATA_COL); +} async function participantIDPrompt() { + const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; return await input({ message: "Select a participant:", validate: async (input) => { @@ -284,7 +285,8 @@ async function participantIDPrompt() { switch (DEPLOYMENT) { case "firebase": - return validateParticipantFirebase(input); + const res = await validateParticipantFirebase(input); + return !res ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } From c8bfe5f94324d5d30aeade18244c983cf6209824 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Thu, 1 Aug 2024 12:43:33 -0500 Subject: [PATCH 06/16] ref: function header, validate return values --- cli.mjs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/cli.mjs b/cli.mjs index a17d512da..e74e8837c 100644 --- a/cli.mjs +++ b/cli.mjs @@ -65,7 +65,7 @@ commander.parse(); // print message if download or delete provided, along with optional args provided if (ACTION != undefined) { console.log( - `${ACTION} data from Firebase ${STUDY_ID == undefined ? "" : `given study ID: ${STUDY_ID}`} ${PARTICIPANT_ID == undefined ? "" : `and participant ID: ${PARTICIPANT_ID}`}` + `${ACTION} data from Firebase given ${STUDY_ID === undefined ? "" : `study ID: ${STUDY_ID}`} ${PARTICIPANT_ID === undefined ? "" : `and participant ID: ${PARTICIPANT_ID}`}` ); } @@ -83,8 +83,8 @@ async function main() { } else { // when args directly passed in through CLI, check if study is valid const hasStudy = await validateStudyFirebase(STUDY_ID); - if (hasStudy != true && ACTION != "register") { - console.error(hasStudy); + if (!hasStudy) { + console.error("Please enter a valid study from your Firestore database"); return; } } @@ -93,9 +93,9 @@ async function main() { PARTICIPANT_ID = await participantIDPrompt(); } else { // when args directly passed in through CLI, check if participant is valid - const hasParticipant = await validateParticipantFirebase(STUDY_ID); - if (hasParticipant != true && ACTION != "register") { - console.error(hasParticipant); + const hasParticipant = await validateParticipantFirebase(PARTICIPANT_ID); + if (!hasParticipant) { + console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); return; } } @@ -287,14 +287,14 @@ async function deploymentPrompt() { /** Prompt the user to enter the ID of a study */ // helper to check if the given study (input) is in firestore -const validateStudyFirebase = async (input) => { - const invalidMessage = "Please enter a valid study from your Firestore database"; +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) ? true : invalidMessage; -}; + return studyIDCollections.find((c) => c.id === PARTICIPANTS_COL); +} async function studyIDPrompt() { + const invalidMessage = "Please enter a valid study from your Firestore database"; return await input({ message: "Select a study:", validate: async (input) => { @@ -305,7 +305,8 @@ async function studyIDPrompt() { } switch (DEPLOYMENT) { case "firebase": - return validateStudyFirebase(input); + const res = await validateStudyFirebase(input); + return !res ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -315,14 +316,14 @@ async function studyIDPrompt() { /** Prompt the user to enter the ID of a participant on the STUDY_ID study */ // helper to check if the given participant (input) is in firestore under study -const validateParticipantFirebase = async (input) => { - const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; +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) ? true : invalidMessage; -}; + return studyIDCollections.find((c) => c.id === DATA_COL); +} async function participantIDPrompt() { + const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; return await input({ message: ACTION == "register" ? "Enter a new participant:" : "Select a participant:", validate: async (input) => { @@ -335,7 +336,8 @@ async function participantIDPrompt() { } switch (DEPLOYMENT) { case "firebase": - return validateParticipantFirebase(input); + const res = await validateParticipantFirebase(input); + return !res ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } From 6c83919d69183ddbc7e375c3c671e34d709073fe Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Thu, 1 Aug 2024 13:01:25 -0500 Subject: [PATCH 07/16] Revert "Merge branch 'add-commander' of https://github.com/brown-ccv/honeycomb into add-commander" This reverts commit c48f5d67cb8fd2b2f6edfba0e33cb75407806576, reversing changes made to c2ec9eb48a3385bddcadd9a31a596d26134ba39f. --- cli.mjs | 126 +------------------------------------------------------- 1 file changed, 2 insertions(+), 124 deletions(-) diff --git a/cli.mjs b/cli.mjs index e74e8837c..2d6b6f41c 100644 --- a/cli.mjs +++ b/cli.mjs @@ -47,19 +47,6 @@ commander STUDY_ID = studyID; PARTICIPANT_ID = participantID; }); - -// register: optional argument studyID and participantID skips relative prompts -commander - .command(`register`) - .argument(`[studyID]`) - .argument(`[participantID]`) - .description(`Register new partipant under study provided a partipantID and studyID`) - .action((studyID, participantID) => { - ACTION = "register"; - STUDY_ID = studyID; - PARTICIPANT_ID = participantID; - }); - commander.parse(); // print message if download or delete provided, along with optional args provided @@ -121,15 +108,6 @@ async function main() { throw INVALID_DEPLOYMENT_ERROR; } break; - case "register": - switch (DEPLOYMENT) { - case "firebase": - await registerDataFirebase(STUDY_ID, PARTICIPANT_ID); - break; - default: - throw INVALID_DEPLOYMENT_ERROR; - } - break; default: throw INVALID_ACTION_ERROR; } @@ -220,24 +198,6 @@ async function deleteDataFirebase() { } else console.log("Skipping deletion"); } -/** -------------------- REGISTER ACTION -------------------- */ - -/** Register new data, write to Firestore */ -async function registerDataFirebase(studyID, participantID) { - const confirmation = await confirmRegisterPrompt(studyID, participantID); - if (confirmation) { - try { - await addStudyAndParticipant(studyID, participantID); - } catch (error) { - console.error( - `Unable to register new participant with participantID ${participantID} under studyID ${studyID}: ` + - error - ); - } - } else console.log("Skipping registration"); - return true; -} - /** -------------------- PROMPTS -------------------- */ /** Prompt the user for the action they are trying to complete */ @@ -249,10 +209,6 @@ async function actionPrompt() { name: "Download data", value: "download", }, - { - name: "Register new participant under study", - value: "register", - }, { name: "Delete data", value: "delete", @@ -299,10 +255,6 @@ async function studyIDPrompt() { message: "Select a study:", validate: async (input) => { if (!input) return invalidMessage; - if (ACTION == "register") { - STUDY_ID = input; - return true; - } switch (DEPLOYMENT) { case "firebase": const res = await validateStudyFirebase(input); @@ -325,15 +277,12 @@ async function validateParticipantFirebase(input) { async function participantIDPrompt() { const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; return await input({ - message: ACTION == "register" ? "Enter a new participant:" : "Select a participant:", + message: "Select a participant:", validate: async (input) => { const invalid = "Please enter a valid participant from your Firestore database"; if (!input) return invalid; else if (input === "*") return true; - if (ACTION == "register") { - PARTICIPANT_ID = input; - return true; - } + switch (DEPLOYMENT) { case "firebase": const res = await validateParticipantFirebase(input); @@ -347,11 +296,6 @@ async function participantIDPrompt() { /** Prompt the user to select one or more experiments of the PARTICIPANT_ID on STUDY_ID */ async function experimentIDPrompt() { - // register: adding/checking for existing new studies will be done in function - if (ACTION == "register") { - return; - } - const dataSnapshot = await getDataRef(STUDY_ID, PARTICIPANT_ID).get(); // Sort experiment choices by most recent first @@ -395,20 +339,6 @@ async function confirmDeletionPrompt() { }); } -async function confirmRegisterPrompt(studyID, participantID) { - const currentParticipants = await getRegisteredParticipantArr(studyID); - const currentParticipantMessage = - currentParticipants.length == 0 - ? "Currently, there are no participants under this study\n" - : `Currently, the participants under this study include: \n${currentParticipants.join("\n")}\n`; - return confirm({ - message: - currentParticipantMessage + - `Continue? adding study with studyID: ${studyID} and participant ID: ${participantID}`, - default: false, - }); -} - /** * Prompts the user to confirm continuation of the CLI, including future conflicts * @param {string} outputFile @@ -446,7 +376,6 @@ async function confirmOverwritePrompt(file, overwriteAll) { /** -------------------- FIRESTORE HELPERS -------------------- */ const RESPONSES_COL = "participant_responses"; -const REG_STUDY_COL = "registered_studies"; const PARTICIPANTS_COL = "participants"; const DATA_COL = "data"; const TRIALS_COL = "trials"; @@ -465,54 +394,3 @@ const getDataRef = (studyID, participantID) => // Get a reference to a participant's specific experiment data document in Firestore const getExperimentRef = (studyID, participantID, experimentID) => getDataRef(studyID, participantID).doc(experimentID); - -// Get a reference to a registered study -const getRegisteredfStudyRef = (studyID) => FIRESTORE.collection(REG_STUDY_COL).doc(studyID); - -// Get current registered participant array under the StudyID -const getRegisteredParticipantArr = (studyID) => { - const res = getRegisteredfStudyRef(studyID) - .get() - .then((data) => { - if (data["_fieldsProto"] != undefined) { - return data["_fieldsProto"]["registered_participants"]["arrayValue"]["values"] - .filter((item) => item.valueType === "stringValue") - .map((item) => item.stringValue); - } else { - return []; - } - }); - - return res; -}; - -// Register new participantID under the provided studyID -const registerNewParticipant = async (studyID, participantID) => { - const currParticipants = await getRegisteredParticipantArr(studyID); - const newParticipantArray = [...currParticipants, participantID]; - const newData = { registered_participants: newParticipantArray }; - getRegisteredfStudyRef(studyID).update(newData); - console.log( - `Successfully added study and participant. Current participantIDs under study ${studyID}: \n${newParticipantArray.join("\n")}` - ); -}; - -// Add new participantID under studyID to Firestore under registered_studies, -// creates new study if studyID doesn't exist -const addStudyAndParticipant = async (studyID, participantID) => { - getRegisteredfStudyRef(studyID) - .get() - .then((data) => { - // study not initated yet - if (data["_fieldsProto"] == undefined) { - getRegisteredfStudyRef(studyID) - .set({ registered_participants: [] }) - .then(() => { - registerNewParticipant(studyID, participantID); - }); - } else { - // study initiated, add to array holding registered participants - registerNewParticipant(studyID, participantID); - } - }); -}; From e25cb92678ba807fedae7bc13451a7ce4a1c2a9a Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:15:55 -0500 Subject: [PATCH 08/16] Update cli.mjs Co-authored-by: Robert Gemma <38439940+RobertGemmaJr@users.noreply.github.com> --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index 2d6b6f41c..aab8ce159 100644 --- a/cli.mjs +++ b/cli.mjs @@ -50,7 +50,7 @@ commander commander.parse(); // print message if download or delete provided, along with optional args provided -if (ACTION != undefined) { +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}`}` ); From f32f1bbf9288f6e93b17ff356f5cdc77f08b29a9 Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:16:03 -0500 Subject: [PATCH 09/16] Update cli.mjs Co-authored-by: Robert Gemma <38439940+RobertGemmaJr@users.noreply.github.com> --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index aab8ce159..b0bd4d683 100644 --- a/cli.mjs +++ b/cli.mjs @@ -60,7 +60,7 @@ if (ACTION !== undefined) { // TODO @brown-ccv #289: Pass CLI arguments with commander (especially for action) async function main() { - if (ACTION == undefined) { + if (ACTION === undefined) { ACTION = await actionPrompt(); } DEPLOYMENT = await deploymentPrompt(); From 4b3cfde0e91c5c3b44d89c7fa0f091d22d4e077d Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:16:10 -0500 Subject: [PATCH 10/16] Update cli.mjs Co-authored-by: Robert Gemma <38439940+RobertGemmaJr@users.noreply.github.com> --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index b0bd4d683..0fcde7d38 100644 --- a/cli.mjs +++ b/cli.mjs @@ -65,7 +65,7 @@ async function main() { } DEPLOYMENT = await deploymentPrompt(); // TODO @brown-ccv #291: Enable downloading all study data at once - if (STUDY_ID == undefined) { + if (STUDY_ID === undefined) { STUDY_ID = await studyIDPrompt(); } else { // when args directly passed in through CLI, check if study is valid From f0233ed8e89ebc9eec7ca6e67e1e110b5ba0836f Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:16:18 -0500 Subject: [PATCH 11/16] Update cli.mjs Co-authored-by: Robert Gemma <38439940+RobertGemmaJr@users.noreply.github.com> --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index 0fcde7d38..8dacf4c1b 100644 --- a/cli.mjs +++ b/cli.mjs @@ -76,7 +76,7 @@ async function main() { } } // TODO @brown-ccv #291: Enable downloading all participant data at once - if (PARTICIPANT_ID == undefined) { + if (PARTICIPANT_ID === undefined) { PARTICIPANT_ID = await participantIDPrompt(); } else { // when args directly passed in through CLI, check if participant is valid From 7089d92c77cdef8e6f7915ae0eda2c742de02d87 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Tue, 6 Aug 2024 11:24:44 -0500 Subject: [PATCH 12/16] ref: change return value name and reorganize validation functions to own section --- cli.mjs | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/cli.mjs b/cli.mjs index 8dacf4c1b..3fb210c81 100644 --- a/cli.mjs +++ b/cli.mjs @@ -69,8 +69,8 @@ async function main() { STUDY_ID = await studyIDPrompt(); } else { // when args directly passed in through CLI, check if study is valid - const hasStudy = await validateStudyFirebase(STUDY_ID); - if (!hasStudy) { + const studyCollection = await validateStudyFirebase(STUDY_ID); + if (!studyCollection) { console.error("Please enter a valid study from your Firestore database"); return; } @@ -80,8 +80,8 @@ async function main() { PARTICIPANT_ID = await participantIDPrompt(); } else { // when args directly passed in through CLI, check if participant is valid - const hasParticipant = await validateParticipantFirebase(PARTICIPANT_ID); - if (!hasParticipant) { + const participantCollection = await validateParticipantFirebase(PARTICIPANT_ID); + if (!participantCollection) { console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); return; } @@ -242,13 +242,6 @@ async function deploymentPrompt() { } /** Prompt the user to enter the ID of a study */ -// 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); -} - async function studyIDPrompt() { const invalidMessage = "Please enter a valid study from your Firestore database"; return await input({ @@ -257,8 +250,8 @@ async function studyIDPrompt() { if (!input) return invalidMessage; switch (DEPLOYMENT) { case "firebase": - const res = await validateStudyFirebase(input); - return !res ? invalidMessage : true; + const studyCollection = await validateStudyFirebase(input); + return !studyCollection ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -267,13 +260,6 @@ async function studyIDPrompt() { } /** Prompt the user to enter the ID of a participant on the STUDY_ID study */ -// 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); -} - async function participantIDPrompt() { const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`; return await input({ @@ -285,8 +271,8 @@ async function participantIDPrompt() { switch (DEPLOYMENT) { case "firebase": - const res = await validateParticipantFirebase(input); - return !res ? invalidMessage : true; + const participantCollection = await validateParticipantFirebase(input); + return !participantCollection ? invalidMessage : true; default: throw INVALID_DEPLOYMENT_ERROR; } @@ -373,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"; From c88c78e9746d4e5f3ea41921f323898218475df9 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Tue, 6 Aug 2024 11:29:23 -0500 Subject: [PATCH 13/16] update package --- package-lock.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4f017e6c..47f00e60c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6316,11 +6316,9 @@ } }, "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": ">=18" } @@ -6981,6 +6979,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" From a7acbf7f991b058bbbe3d548fb3aa919a9a9a087 Mon Sep 17 00:00:00 2001 From: YUUU23 Date: Thu, 8 Aug 2024 15:44:10 -0500 Subject: [PATCH 14/16] ref: change error return to exit on error --- cli.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli.mjs b/cli.mjs index 22cdce38a..dca4d1fc0 100644 --- a/cli.mjs +++ b/cli.mjs @@ -72,7 +72,7 @@ async function main() { const studyCollection = await validateStudyFirebase(STUDY_ID); if (!studyCollection) { console.error("Please enter a valid study from your Firestore database"); - return; + process.exit(1); } } // TODO @brown-ccv #291: Enable downloading all participant data at once @@ -83,7 +83,7 @@ async function main() { const participantCollection = await validateParticipantFirebase(PARTICIPANT_ID); if (!participantCollection) { console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); - return; + process.exit(1); } } EXPERIMENT_IDS = await experimentIDPrompt(); From 9239d7a84d14d61c6696b1ba3fb257061ec88444 Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Wed, 14 Aug 2024 01:37:41 -0500 Subject: [PATCH 15/16] simplify comparison logic. Update cli.mjs Co-authored-by: Ellen Duong --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index dca4d1fc0..f7e0d8675 100644 --- a/cli.mjs +++ b/cli.mjs @@ -251,7 +251,7 @@ async function studyIDPrompt() { switch (DEPLOYMENT) { case "firebase": const studyCollection = await validateStudyFirebase(input); - return !studyCollection ? invalidMessage : true; + return studyCollection || invalidMessage; default: throw INVALID_DEPLOYMENT_ERROR; } From 1cc9e922a9d0930f3c8519ef457a8a69a991276a Mon Sep 17 00:00:00 2001 From: Megan <113795130+YUUU23@users.noreply.github.com> Date: Wed, 14 Aug 2024 01:46:37 -0500 Subject: [PATCH 16/16] simplify comparison logic. Update cli.mjs Co-authored-by: Ellen Duong --- cli.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.mjs b/cli.mjs index f7e0d8675..b30b7ff81 100644 --- a/cli.mjs +++ b/cli.mjs @@ -272,7 +272,7 @@ async function participantIDPrompt() { switch (DEPLOYMENT) { case "firebase": const participantCollection = await validateParticipantFirebase(input); - return !participantCollection ? invalidMessage : true; + return participantCollection || invalidMessage; default: throw INVALID_DEPLOYMENT_ERROR; }