@levante-framework/firekit
Version:
A library to facilitate Firebase authentication and Firestore interaction for LEVANTE apps
268 lines (267 loc) • 10 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoarAppkit = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const auth_1 = require("firebase/auth");
const firestore_1 = require("firebase/firestore");
const storage_1 = require("firebase/storage");
const run_1 = require("./run");
const task_1 = require("./task");
const user_1 = require("./user");
const util_1 = require("../util");
/**
* The RoarAppkit class is the main entry point for ROAR apps using the ROAR
* Firestore API. It represents multiple linked Firestore documents and
* provides methods for interacting with them.
*/
class RoarAppkit {
/**
* Create a RoarAppkit.
*
* @param {AppkitInput} input
* @param {UserInfo} input.userInfo - The user input object
* @param {TaskVariantForAssessment} input.taskInfo - The task input object
* @param {OrgLists} input.assigningOrgs - The IDs of the orgs to which this run belongs
* @param {OrgLists} input.readOrgs - The IDs of the orgs that can read this run
* @param {string} input.assignmentId - The ID of the assignment this run belongs to
* @param {string} input.runId - The ID of the run. If undefined, a new run will be created.
*/
constructor({ firebaseProject, firebaseConfig, userInfo, taskInfo, assigningOrgs, readOrgs, assignmentId, runId, }) {
if (!firebaseProject && !firebaseConfig) {
throw new Error('You must provide either a firebaseProjectKit or firebaseConfig');
}
if (firebaseProject && firebaseConfig) {
throw new Error('You must provide either a firebaseProjectKit or firebaseConfig, not both');
}
this.firebaseConfig = firebaseConfig;
this.firebaseProject = firebaseProject;
this._userInfo = userInfo;
this._taskInfo = taskInfo;
this._assigningOrgs = assigningOrgs;
this._readOrgs = readOrgs ?? assigningOrgs;
this._assignmentId = assignmentId;
this._runId = runId;
this._authenticated = false;
this._initialized = false;
this._started = false;
}
async _init() {
if (this.firebaseConfig) {
this.firebaseProject = await (0, util_1.initializeFirebaseProject)(this.firebaseConfig, 'admin');
}
(0, auth_1.onAuthStateChanged)(this.firebaseProject.auth, (user) => {
this._authenticated = Boolean(user);
});
this.user = new user_1.RoarAppUser({
...this._userInfo,
db: this.firebaseProject.db,
});
this.task = new task_1.RoarTaskVariant({
...this._taskInfo,
db: this.firebaseProject.db,
});
this.run = new run_1.RoarRun({
user: this.user,
task: this.task,
assigningOrgs: this._assigningOrgs,
readOrgs: this._readOrgs,
assignmentId: this._assignmentId,
runId: this._runId,
});
await this.user.init();
this._initialized = true;
}
get authenticated() {
return this._authenticated;
}
/**
* Update the user's data (both locally and in Firestore).
* @param {object} input
* @param {string[]} input.tasks - The tasks to be added to the user doc
* @param {string[]} input.variants - The variants to be added to the user doc
* @param {string} input.assessmentPid - The assessment PID of the user
* @param {*} input.userMetadata - Any additional user metadata
* @method
* @async
*/
async updateUser({ tasks, variants, assessmentPid, ...userMetadata }) {
if (!this._initialized) {
await this._init();
}
if (!this.authenticated) {
throw new Error('User must be authenticated to update their own data.');
}
return await this.user.updateUser({ tasks, variants, assessmentPid, ...userMetadata });
}
/**
* Start the ROAR run. Push the task and run info to Firestore.
* Call this method before starting the jsPsych experiment.
* @method
* @async
*/
async startRun(additionalRunMetadata) {
if (!this._initialized) {
await this._init();
}
if (!this.authenticated) {
throw new Error('User must be authenticated to start a run.');
}
return await this.run.startRun(additionalRunMetadata).then(() => (this._started = true));
}
/**
* Update the ROAR task's game parameters.
* This must be called after the startRun() method.
*
* @method
* @async
*/
async updateTaskParams(newParams) {
if (this._started) {
const oldVariantId = this.task.variantId;
return await this.task.updateTaskParams(newParams)
.then(() => {
return (0, firestore_1.updateDoc)(this.user.userRef, { variants: (0, firestore_1.arrayRemove)(oldVariantId) });
})
.then(() => {
return (0, firestore_1.updateDoc)(this.user.userRef, { variants: (0, firestore_1.arrayUnion)(this.task.variantId) });
})
.then(() => {
return (0, firestore_1.updateDoc)(this.run.runRef, { variantId: this.task.variantId });
});
}
else {
throw new Error('This run has not started. Use the startRun method first.');
}
}
/**
* Update the engagement flags for the current run.
*
* @param {string[]} flagNames - The names of the engagement flags to add.
* @param {boolean} markAsReliable - Whether or not to mark the run as reliable, defaults to false
* @method
* @async
*
* Please note that calling this function with a new set of engagement flags will
* overwrite the previous set.
*/
async updateEngagementFlags(flagNames, markAsReliable = false, reliableByBlock = undefined) {
if (this._started) {
return await this.run.addEngagementFlags(flagNames, markAsReliable, reliableByBlock);
}
else {
throw new Error('This run has not started. Use the startRun method first.');
}
}
/**
* Finish the ROAR run by marking it as finished in Firestore.
* Call this method after the jsPsych experiment finishes. For example:
*
* ```javascript
* jsPsych.init({
* timeline: exp,
* on_finish: function(data) {
* firekit.finishRun();
* }
* });
* ```
* @method
* @async
* @param {Object} [finishingMetaData={}] - Optional metadata to include when marking the run as complete.
* @returns {Promise<boolean | undefined>} - Resolves when the run has been marked as complete.
* @throws {Error} - Throws an error if the run has not been started yet.
*/
async finishRun(finishingMetaData = {}) {
if (this._started) {
return await this.run.finishRun(finishingMetaData);
}
else {
throw new Error('This run has not started. Use the startRun method first.');
}
}
/**
* Abort the ROAR run, preventing any further writes to Firestore.
* @method
*/
abortRun() {
if (this._started) {
this.run.abortRun();
}
else {
throw new Error('This run has not started. Use the startRun method first.');
}
}
/**
* Add new trial data to this run on Firestore.
*
* ROAR expects certain data to be added to each trial:
* - assessment_stage: string, either practice_response or test_response
* - correct: boolean, whether the correct answer was correct
* - subtask: string (optional), the name of the subtask
* - thetaEstimate: number (optional), the ability estimate for adaptive assessments
* - thetaSE: number (optional), the standard error of the ability estimate for adaptive assessments
*
* This method can be added to individual jsPsych trials by calling it from
* the `on_finish` function, like so:
*
* ```javascript
* var trial = {
* type: 'image-keyboard-response',
* stimulus: 'imgA.png',
* on_finish: function(data) {
* firekit.addTrialData(data);
* }
* };
* ```
*
* Or you can call it from all trials in a jsPsych
* timeline by calling it from the `on_data_update` callback. In the latter
* case, you can avoid saving extraneous trials by conditionally calling
* this method based on the data. For example:
*
* ```javascript
* const timeline = [
* // A fixation trial; don't save to Firestore
* {
* type: htmlKeyboardResponse,
* stimulus: '<div style="font-size:60px;">+</div>',
* choices: "NO_KEYS",
* trial_duration: 500,
* },
* // A stimulus and response trial; save to Firestore
* {
* type: imageKeyboardResponse,
* stimulus: 'imgA.png',
* data: { save: true },
* }
* ]
* jsPsych.init({
* timeline: timeline,
* on_data_update: function(data) {
* if (data.save) {
* firekit.addTrialData(data);
* }
* }
* });
* ```
*
* @method
* @async
* @param {*} trialData - An object containing trial data.
*/
async writeTrial(trialData, computedScoreCallback) {
if (this._started) {
return await this.run.writeTrial(trialData, computedScoreCallback);
}
else {
throw new Error('This run has not started. Use the startRun method first.');
}
}
async getStorageDownloadUrl(filePath) {
if (!this._initialized) {
await this._init();
}
const storageRef = (0, storage_1.ref)(this.firebaseProject.storage, filePath);
return (0, storage_1.getDownloadURL)(storageRef);
}
}
exports.RoarAppkit = RoarAppkit;