-
Notifications
You must be signed in to change notification settings - Fork 8
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
Add cli register #517
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ac0491b
add back register
YUUU23 715766f
ref: description, strictly equal
YUUU23 62dbe64
ensure everything is strictly equal in cli script
YUUU23 998e917
Update cli.mjs
YUUU23 f1b4f34
Merge branch 'add-commander' into add-CLI-register
YUUU23 9ec503e
Merge branch 'add-commander' into add-CLI-register
YUUU23 8bfce24
ref: async functions, remove duplicate helpers
YUUU23 a26eedb
fix: logical expression to change back to casting to bool for prompt …
YUUU23 3cbb4da
merge in new changes
YUUU23 076038d
add: add back commander and util functions
YUUU23 9b947b1
add: add back validation functions and dependencies
YUUU23 a358f21
add: add back prompt checking
YUUU23 58fceba
Merge pull request #531 from brown-ccv/add-commander
YUUU23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -47,19 +47,32 @@ 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; | ||
}); | ||
|
||
/** -------------------- MAIN -------------------- */ | ||
|
||
// TODO @brown-ccv #289: Pass CLI arguments with commander (especially for action) | ||
async function main() { | ||
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}`}` | ||
); | ||
} | ||
|
||
if (ACTION === undefined) { | ||
ACTION = await actionPrompt(); | ||
} | ||
|
@@ -70,7 +83,7 @@ async function main() { | |
} else { | ||
// when args directly passed in through CLI, check if study is valid | ||
const studyCollection = await validateStudyFirebase(STUDY_ID); | ||
if (!studyCollection) { | ||
if (!studyCollection && ACTION !== "register") { | ||
console.error("Please enter a valid study from your Firestore database"); | ||
process.exit(1); | ||
} | ||
|
@@ -81,7 +94,7 @@ async function main() { | |
} else { | ||
// when args directly passed in through CLI, check if participant is valid | ||
const participantCollection = await validateParticipantFirebase(PARTICIPANT_ID); | ||
if (!participantCollection) { | ||
if (!participantCollection && ACTION !== "register") { | ||
console.error(`Please enter a valid participant on the study "${STUDY_ID}"`); | ||
process.exit(1); | ||
} | ||
|
@@ -108,6 +121,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; | ||
} | ||
|
@@ -198,6 +220,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 */ | ||
|
@@ -209,6 +249,10 @@ async function actionPrompt() { | |
name: "Download data", | ||
value: "download", | ||
}, | ||
{ | ||
name: "Register new participant under study", | ||
value: "register", | ||
}, | ||
{ | ||
name: "Delete data", | ||
value: "delete", | ||
|
@@ -242,12 +286,23 @@ 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"; | ||
// 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; | ||
if (ACTION === "register") { | ||
STUDY_ID = input; | ||
return true; | ||
} | ||
switch (DEPLOYMENT) { | ||
case "firebase": | ||
const studyCollection = await validateStudyFirebase(input); | ||
|
@@ -260,15 +315,25 @@ 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}"`; | ||
// 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:", | ||
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 participantCollection = await validateParticipantFirebase(input); | ||
|
@@ -282,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 | ||
|
@@ -325,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 | ||
|
@@ -377,6 +461,7 @@ async function validateParticipantFirebase(input) { | |
/** -------------------- 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"; | ||
|
@@ -395,3 +480,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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be an async function? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
}); | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.