jspsych-datamanager
Version:
A package to manage data for jsPsych experiments. Currently Firebase and Supabase are supported.
248 lines (240 loc) • 9.18 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
SupabaseManager: () => SupabaseManager
});
module.exports = __toCommonJS(index_exports);
// src/supabase.ts
var import_supabase_js = require("@supabase/supabase-js");
var import_core = require("@jspsych-datamanager/core");
var SupabaseManager = class extends import_core.DataManager {
/**
* Creates a new SupabaseManager instance
* @param supabaseConfig Supabase configuration object
* @param options Additional options for initialization
*/
constructor(supabaseConfig, options = {}) {
super(options.metadata);
this.numberOfOperations = 0;
this.initialized = false;
this.pendingTrials = [];
this.supabase = (0, import_supabase_js.createClient)(supabaseConfig.url, supabaseConfig.anonKey);
this.tableName = options.tableName || "experiments";
this.rowId = options.rowId;
if (this.rowId) {
this.initialized = true;
console.log(`[SupabaseManager] Using provided row ID: ${this.rowId}`);
}
}
/**
* Initializes the experiment data in Supabase
* @param additionalData Additional data to include in the experiment document
* @throws {Error} If initialization fails
*/
async initializeExperiment(additionalData = {}) {
try {
if (this.initialized && this.rowId) {
console.log(`[SupabaseManager] Updating existing row with ID: ${this.rowId}`);
const initialData2 = {
...this.metadata,
...additionalData,
updated_at: (/* @__PURE__ */ new Date()).toISOString()
};
const { error: error2 } = await this.supabase.from(this.tableName).update(initialData2).eq("id", this.rowId);
if (error2) {
this.handleRlsError(error2);
throw error2;
}
this.numberOfOperations++;
console.log("[SupabaseManager] Data successfully updated!");
return;
}
const initialData = {
...this.metadata,
trials: [],
...additionalData
};
const { data, error } = await this.supabase.from(this.tableName).insert(initialData).select();
if (error) {
this.handleRlsError(error);
throw error;
}
if (data && data.length > 0 && data[0].id) {
this.rowId = data[0].id;
this.initialized = true;
console.log(`[SupabaseManager] Row created with ID: ${this.rowId}`);
if (this.pendingTrials.length > 0) {
console.log(`[SupabaseManager] Processing ${this.pendingTrials.length} pending trials...`);
const trials = [...this.pendingTrials];
this.pendingTrials = [];
for (const trial of trials) {
await this.addTrialData(trial).catch((e) => {
console.error("[SupabaseManager] Error processing pending trial:", e);
});
}
}
} else {
console.error("[SupabaseManager] No row ID returned from insert operation");
throw new Error("Failed to get row ID from insert operation");
}
this.numberOfOperations++;
console.log("[SupabaseManager] Data successfully initialized!");
} catch (error) {
console.error("[SupabaseManager] Error initializing data:", error);
throw new Error("Failed to initialize experiment data: " + (error instanceof Error ? error.message : String(error)));
}
}
/**
* Adds a new trial to the experiment data
* @param trialData The trial data to add
* @throws {Error} If storing the trial fails
*/
async addTrialData(trialData) {
if (!this.initialized) {
console.log("[SupabaseManager] Not initialized yet, storing trial for later processing");
this.pendingTrials.push(trialData);
return;
}
if (!this.rowId) {
console.log("[SupabaseManager] No row ID available, storing trial for later processing");
this.pendingTrials.push(trialData);
this.initialized = false;
await this.initializeExperiment();
return;
}
try {
const { data: currentData, error: fetchError } = await this.supabase.from(this.tableName).select("trials").eq("id", this.rowId).single();
if (fetchError) {
console.error("[SupabaseManager] Error fetching current data:", fetchError);
this.handleRlsError(fetchError);
if (fetchError.code === "PGRST116") {
console.log("[SupabaseManager] Row not found, attempting to reinitialize...");
this.initialized = false;
this.rowId = void 0;
this.pendingTrials.push(trialData);
await this.initializeExperiment();
return;
}
throw fetchError;
}
const updatedTrials = [...currentData?.trials || [], trialData];
const { error: updateError } = await this.supabase.from(this.tableName).update({
trials: updatedTrials,
updated_at: (/* @__PURE__ */ new Date()).toISOString()
}).eq("id", this.rowId);
if (updateError) {
console.error("[SupabaseManager] Error updating data:", updateError);
this.handleRlsError(updateError);
throw updateError;
}
this.numberOfOperations++;
console.log("[SupabaseManager] Added trial data:", trialData);
} catch (error) {
console.error("[SupabaseManager] Error storing trial data:", error);
if (error instanceof Error && error.message.includes("not found")) {
console.log("[SupabaseManager] Row not found, attempting to reinitialize...");
this.initialized = false;
this.rowId = void 0;
this.pendingTrials.push(trialData);
await this.initializeExperiment();
return;
}
throw new Error("Failed to store trial data: " + (error instanceof Error ? error.message : String(error)));
}
}
/**
* Gets the total number of operations performed
* @returns The number of operations performed
*/
getNumberOfOperations() {
return this.numberOfOperations;
}
/**
* Creates a callback function for jsPsych's on_data_update event
* @returns A function that handles trial data updates
*/
createDataUpdateCallback() {
return (data) => {
if (data.no_upload) {
delete data.no_upload;
return data;
}
this.addTrialData(data).catch((error) => {
console.error("[SupabaseManager] Error in data update callback:", error);
});
return data;
};
}
/**
* Creates a callback function for jsPsych's on_finish event
* @returns A function that handles experiment completion
*/
createFinishCallback() {
return () => {
console.log("[SupabaseManager] Total operations performed:", this.getNumberOfOperations());
if (this.pendingTrials.length > 0) {
console.warn(`[SupabaseManager] Warning: ${this.pendingTrials.length} trials were not processed`);
}
};
}
/**
* Checks if the manager has been properly initialized
* @returns True if initialized, false otherwise
*/
isInitialized() {
return this.initialized && !!this.rowId;
}
/**
* Handles RLS policy error by providing helpful information on how to fix it
* @param error The error object from Supabase
* @private
*/
handleRlsError(error) {
if (error && error.code === "42501" && error.message.includes("violates row-level security policy")) {
console.error(`
=================================================================
ROW LEVEL SECURITY POLICY VIOLATION DETECTED
This error occurs because you don't have the proper RLS policies
set up for your Supabase table "${this.tableName}".
To fix this, follow these steps:
1. Go to your Supabase dashboard
2. Navigate to "Authentication" \u2192 "Policies"
3. Find your "${this.tableName}" table
4. Add the following policies:
- For INSERT:
* Create a new policy named "Enable inserts for all users"
* Choose "INSERT" for the operation
* Set USING expression to "true"
- For SELECT:
* Create a new policy named "Enable select for all users"
* Choose "SELECT" for the operation
* Set USING expression to "true"
You may also need policies for UPDATE and DELETE operations
if your application needs to perform these actions.
=================================================================
`);
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SupabaseManager
});