Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cli register #517

Merged
merged 13 commits into from
Aug 15, 2024
182 changes: 152 additions & 30 deletions cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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
// default: [download | delete | register ] not provided, run main() as usual continuing with prompting
commander.action(() => {});

// download: optional argument studyID and participantID skips relative prompts
Expand All @@ -47,42 +47,57 @@ commander
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}`}`
);
}
// 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; new study will be created if not found`
)
.action((studyID, participantID) => {
ACTION = "register";
STUDY_ID = studyID;
PARTICIPANT_ID = participantID;
});

commander.parse();

/** -------------------- MAIN -------------------- */

// TODO @brown-ccv #289: Pass CLI arguments with commander (especially for action)
async function main() {
YUUU23 marked this conversation as resolved.
Show resolved Hide resolved
if (ACTION == undefined) {
// 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}`}`
);
}

if (ACTION === undefined) {
ACTION = await actionPrompt();
}
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
const hasStudy = await validateStudyFirebase(STUDY_ID);
if (!hasStudy) {
console.error("Please enter a valid study from your Firestore database");
if (hasStudy !== true && ACTION !== "register") {
console.error(hasStudy);
return;
}
}
// 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
const hasParticipant = await validateParticipantFirebase(PARTICIPANT_ID);
if (!hasParticipant) {
console.error(`Please enter a valid participant on the study "${STUDY_ID}"`);
const hasParticipant = await validateParticipantFirebase(STUDY_ID);
if (hasParticipant !== true && ACTION !== "register") {
console.error(hasParticipant);
return;
}
}
Expand All @@ -108,6 +123,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;
}
Expand Down Expand Up @@ -198,6 +222,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 */
Expand All @@ -209,6 +251,10 @@ async function actionPrompt() {
name: "Download data",
value: "download",
},
{
name: "Register new participant under study",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can they register to new participants AND new studies or just new participants on an existing study?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should automatically register the new study if the study is not found! I will update this description to make it more clear.

value: "register",
},
{
name: "Delete data",
value: "delete",
Expand Down Expand Up @@ -243,22 +289,25 @@ 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) {
const validateStudyFirebase = async (input) => {
const invalidMessage = "Please enter a valid study from your Firestore database";
// 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);
}
return studyIDCollections.find((c) => c.id === PARTICIPANTS_COL) ? true : invalidMessage;
};

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;
if (ACTION === "register") {
STUDY_ID = input;
return true;
}
switch (DEPLOYMENT) {
case "firebase":
const res = await validateStudyFirebase(input);
return !res ? invalidMessage : true;
return validateStudyFirebase(input);
default:
throw INVALID_DEPLOYMENT_ERROR;
}
Expand All @@ -268,25 +317,27 @@ 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) {
const validateParticipantFirebase = async (input) => {
const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`;
// 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);
}
return studyIDCollections.find((c) => c.id === DATA_COL) ? true : invalidMessage;
};

async function participantIDPrompt() {
const invalidMessage = `Please enter a valid participant on the study "${STUDY_ID}"`;
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":
const res = await validateParticipantFirebase(input);
return !res ? invalidMessage : true;
return validateParticipantFirebase(input);
default:
throw INVALID_DEPLOYMENT_ERROR;
}
Expand All @@ -296,6 +347,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
Expand Down Expand Up @@ -339,6 +395,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
Expand Down Expand Up @@ -376,6 +446,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";
Expand All @@ -394,3 +465,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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be an async function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just updated this too!

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) => {
eldu marked this conversation as resolved.
Show resolved Hide resolved
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);
}
});
};
Loading