UNPKG

jspsych-datamanager

Version:

A package to manage data for jsPsych experiments. Currently Firebase and Supabase are supported.

221 lines (215 loc) 8.1 kB
// src/supabase.ts import { createClient } from "@supabase/supabase-js"; import { DataManager } from "@jspsych-datamanager/core"; var SupabaseManager = class extends 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 = 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. ================================================================= `); } } }; export { SupabaseManager };