diff --git a/src/webcomponents/HabitCounter/HabitCounter.ts b/src/webcomponents/HabitCounter/HabitCounter.ts index 641a3a5..031cf00 100644 --- a/src/webcomponents/HabitCounter/HabitCounter.ts +++ b/src/webcomponents/HabitCounter/HabitCounter.ts @@ -20,6 +20,9 @@ export class HabitCounter extends HTMLElement { public initialDate: string = ""; public emoji: string = ""; public table: string = ""; + public habitIdCol: string = ""; + public valueCol: string = ""; + public dateCol: string = ""; public currentValue: number = 0; private _initialLoadTriggered: boolean = false; private _isInitialized: boolean = false; @@ -36,7 +39,6 @@ export class HabitCounter extends HTMLElement { // --- Lifecycle --- connectedCallback() { if (this._isInitialized) return; - //& console.log("[HabitCounter Component] connectedCallback - Initializing..."); //~ Create handlers bound to this instance *before* building DOM const handlers = { @@ -47,12 +49,10 @@ export class HabitCounter extends HTMLElement { this.uiElements = buildHabitCounterDOM(this.attachShadow({ mode: "open" }), handlers); //~ Build DOM via helper applyHabitCounterStyles(this.shadowRoot!); //~ Apply styles via helper this._isInitialized = true; - //& console.log("[HabitCounter Component] Initialization complete."); } // --- Public Methods --- public setDbService(service: DBService): void { - //& console.log("[HabitCounter Component] setDbService called."); if (!service) { console.error("[HabitCounter Component] Invalid DBService provided."); this.showErrorState("Setup Error"); //~ Use helper @@ -70,29 +70,27 @@ export class HabitCounter extends HTMLElement { // --- Internal Orchestration --- private _readAttributesAndInitLoad(): void { - //& console.log("[HabitCounter Component] _readAttributesAndInitLoad running."); this.habitKey = this.getAttribute("habit") ?? ""; this.initialDate = this.getAttribute("date") ?? "@date"; this.emoji = this.getAttribute("emoji") ?? "❓"; this.table = this.getAttribute("table") ?? ""; - + this.habitIdCol = this.getAttribute("data-habit-id-col") ?? ""; + this.valueCol = this.getAttribute("data-value-col") ?? ""; + this.dateCol = this.getAttribute("data-date-col") ?? ""; + // --- Defer UI update and load trigger slightly --- requestAnimationFrame(() => { - //& console.log(`[HabitCounter Component ${this.habitKey}] Deferred execution starting.`); - //? Now access uiElements, which should be reliably assigned. - console.log(`[HabitCounter Component ${this.habitKey}] DEBUG: Inspecting this.uiElements within deferred call:`, this.uiElements); - updateStaticUI(this.uiElements, this.emoji, this.habitKey); //~ Update static parts of UI - if (!this.table) { - console.warn(`[HabitCounter Component ${this.habitKey}] Table attribute missing.`); - this.showErrorState("No table"); - return; + if (!this.table || !this.habitKey || !this.habitIdCol || !this.valueCol || !this.dateCol) { + console.warn(`[HabitCounter Component ${this.habitKey}] Missing one or more required attributes: table, habit, data-habit-id-col, data-value-col, data-date-col.`); + this.showErrorState("Config Error"); + updateStaticUI(this.uiElements, this.emoji, this.habitKey || "Config Error"); + return; // Stop initialization if config is bad } if (!this._initialLoadTriggered) { this._initialLoadTriggered = true; - //& console.log(`[HabitCounter Component ${this.habitKey}] Triggering initial loadHabitData...`); loadHabitData(this).catch(err => console.error("Unhandled error during initial load:", err)); //~ Load data via helper } else { console.log(`[HabitCounter Component ${this.habitKey}] Initial load already triggered.`); @@ -102,13 +100,11 @@ export class HabitCounter extends HTMLElement { //? Public method called by click handlers via instance reference public async _updateData(delta: number): Promise { - //& console.log(`[HabitCounter Component ${this.habitKey}] _updateData called with delta: ${delta}`); await updateHabitData(this, delta); //~ Update data via helper } //? Public method called by data helpers to update display public _updateDisplay(newValue: number): void { - //& console.log(`[HabitCounter Component ${this.habitKey}] _updateDisplay called with value: ${newValue}`); this.currentValue = newValue; //? Update internal state first updateDisplayValue(this.uiElements, newValue); //~ Update DOM via helper this.clearErrorState(); //? Clear errors on successful update diff --git a/src/webcomponents/HabitCounter/HabitCounter.types.ts b/src/webcomponents/HabitCounter/HabitCounter.types.ts index c829d0a..f10cb80 100644 --- a/src/webcomponents/HabitCounter/HabitCounter.types.ts +++ b/src/webcomponents/HabitCounter/HabitCounter.types.ts @@ -1,16 +1,20 @@ -//? Defines the expected structure of a record fetched from the habit table. +//? Expected structure returned by the data service fetch method. export interface HabitRecord { - value: number; //^ Assuming the column name for the count is 'value' + value: number; } -//? Defines the properties needed to identify and update a habit entry. +//? Base arguments for identifying a habit entry, now includes column names. export interface HabitDataArgs { table: string; - habitKey: string; //^ Assuming the column name for the habit identifier is 'habitKey' + habitKey: string; //? The *value* identifying the specific habit date: string; + //^ Column Names specified by the user + habitIdCol: string; + valueCol: string; + dateCol: string; } -//? Combined arguments including the new value for updates. +//? Arguments for updating, inheriting base args and adding the new value. export interface UpdateHabitDataArgs extends HabitDataArgs { newValue: number; } \ No newline at end of file diff --git a/src/webcomponents/HabitCounter/data/calculateEffectiveDate.ts b/src/webcomponents/HabitCounter/data/calculateEffectiveDate.ts index 429f44c..4206f89 100644 --- a/src/webcomponents/HabitCounter/data/calculateEffectiveDate.ts +++ b/src/webcomponents/HabitCounter/data/calculateEffectiveDate.ts @@ -7,6 +7,5 @@ import { pluginState } from "../../../pluginState"; */ export function calculateEffectiveDate(initialDate: string): string { const effectiveDate = initialDate === "@date" ? pluginState.selectedDate : initialDate; - //& console.log(`[CalculateEffectiveDate] Calculated: ${effectiveDate} (Initial: ${initialDate}, Global: ${pluginState.selectedDate})`); return effectiveDate; } \ No newline at end of file diff --git a/src/webcomponents/HabitCounter/data/loadHabitData.ts b/src/webcomponents/HabitCounter/data/loadHabitData.ts index 6b461ca..1c63d66 100644 --- a/src/webcomponents/HabitCounter/data/loadHabitData.ts +++ b/src/webcomponents/HabitCounter/data/loadHabitData.ts @@ -6,17 +6,19 @@ interface LoadComponentInstance { habitDataService: HabitDataService | null; table: string; habitKey: string; - initialDate: string; // Needed for effective date calculation - _updateDisplay: (value: number) => void; // Method to update UI - showErrorState: (message: string) => void; // Method to show error UI + initialDate: string; + habitIdCol: string; + valueCol: string; + dateCol: string; + _updateDisplay: (value: number) => void; + showErrorState: (message: string) => void; } /** - * //? Orchestrates loading the habit data using the HabitDataService. + * Orchestrates loading the habit data using the HabitDataService. * @param instance - The HabitCounter component instance. */ export async function loadHabitData(instance: LoadComponentInstance): Promise { - //& console.log(`[LoadHabitData ${instance.habitKey}] loadHabitData running.`); if (!instance.habitDataService || !instance.table || !instance.habitKey) { console.error(`[LoadHabitData ${instance.habitKey}] Cannot load data: Missing service, table, or habitKey.`); instance.showErrorState("Load Error"); @@ -26,12 +28,14 @@ export async function loadHabitData(instance: LoadComponentInstance): Promise void; // Method to update UI - showErrorState: (message: string) => void; // Method to show error UI - clearErrorState: () => void; // Method to clear error UI + initialDate: string; + currentValue: number; + habitIdCol: string; + valueCol: string; + dateCol: string; + _updateDisplay: (value: number) => void; + showErrorState: (message: string) => void; + clearErrorState: () => void; } @@ -21,7 +24,6 @@ interface UpdateComponentInstance { * @param delta - The amount to change the value by (+1 or -1). */ export async function updateHabitData(instance: UpdateComponentInstance, delta: number): Promise { - //& console.log(`[UpdateHabitData ${instance.habitKey}] updateHabitData running with delta: ${delta}.`); if (!instance.habitDataService || !instance.table || !instance.habitKey) { console.error(`[UpdateHabitData ${instance.habitKey}] Cannot update data: Missing service, table, or habitKey.`); instance.showErrorState("Save Error"); @@ -35,13 +37,15 @@ export async function updateHabitData(instance: UpdateComponentInstance, delta: const args = { table: instance.table, habitKey: instance.habitKey, - date: calculateEffectiveDate(instance.initialDate), //~ Delegate date calculation + date: calculateEffectiveDate(instance.initialDate), newValue: newValue, + habitIdCol: instance.habitIdCol, + valueCol: instance.valueCol, + dateCol: instance.dateCol, }; try { await instance.habitDataService.updateHabitValue(args); - //& console.log(`[UpdateHabitData ${instance.habitKey}] Data updated successfully to: ${newValue}`); instance._updateDisplay(newValue); //? Update UI only on successful save instance.clearErrorState(); } catch (error) { diff --git a/src/webcomponents/HabitCounter/registerHabitCounter.ts b/src/webcomponents/HabitCounter/registerHabitCounter.ts index ae3b01b..d8af13e 100644 --- a/src/webcomponents/HabitCounter/registerHabitCounter.ts +++ b/src/webcomponents/HabitCounter/registerHabitCounter.ts @@ -22,22 +22,17 @@ export const registerHabitCounter = (el: HTMLElement, dbService: DBService) => { const date = placeholderEl.dataset.date; const emoji = placeholderEl.dataset.emoji; const table = placeholderEl.dataset.table; + const habitIdCol = placeholderEl.dataset.habitIdCol; + const valueCol = placeholderEl.dataset.valueCol; + const dateCol = placeholderEl.dataset.dateCol; - // --- Validation --- - if (!habitKey) { - console.warn("[HabitCounter Reg] Placeholder missing data-habit:", placeholderEl); - placeholderEl.textContent = "[Missing habit name]"; + if (!habitKey || !table || !habitIdCol || !valueCol || !dateCol) { + console.warn("[HabitCounter Reg] Placeholder missing one or more required data attributes (habit, table, habit-id-col, value-col, date-col):", placeholderEl); + placeholderEl.textContent = "[Config Error]"; return; } - if (!table) { - console.warn(`[HabitCounter Reg] Placeholder for habit "${habitKey}" missing data-table:`, placeholderEl); - placeholderEl.textContent = "[Missing table name]"; - return; - } - // --- End Validation --- try { - //& console.log(`[HabitCounter Reg] Creating component for habit: ${habitKey}, table: ${table}`); const component = document.createElement("habit-counter") as HabitCounter; //^ Set attributes that the component will read later @@ -46,14 +41,14 @@ export const registerHabitCounter = (el: HTMLElement, dbService: DBService) => { if (date) component.setAttribute("date", date); else component.setAttribute("date", "@date"); if (emoji) component.setAttribute("emoji", emoji); + component.setAttribute("data-habit-id-col", habitIdCol); + component.setAttribute("data-value-col", valueCol); + component.setAttribute("data-date-col", dateCol); - //& console.log(`[HabitCounter Reg] Injecting DBService for habit: ${habitKey}`); //? Inject the DBService dependency - component reads attributes inside this call now component.setDbService(dbService); placeholderEl.replaceWith(component); - //& console.log(`[HabitCounter Reg] Replacement successful for habit: ${habitKey}`); - } catch (error) { console.error(`[HabitCounter Reg] Error processing placeholder for habit "${habitKey}":`, error, placeholderEl); placeholderEl.textContent = "[Error Loading Counter]"; diff --git a/src/webcomponents/HabitCounter/services/HabitDataService.ts b/src/webcomponents/HabitCounter/services/HabitDataService.ts index d710c87..ed6a98c 100644 --- a/src/webcomponents/HabitCounter/services/HabitDataService.ts +++ b/src/webcomponents/HabitCounter/services/HabitDataService.ts @@ -1,61 +1,71 @@ -import { DBService } from "src/dbService"; +import { DBService } from "../../../DBService"; import { HabitRecord, HabitDataArgs, UpdateHabitDataArgs } from "../HabitCounter.types"; import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier"; import { validateFetchArgs, validateUpdateArgs } from "./utils/validateHabitArgs"; -//? Service for database interactions related to habits. export class HabitDataService { private dbService: DBService; constructor(dbService: DBService) { if (!dbService) throw new Error("HabitDataService requires a valid DBService instance."); this.dbService = dbService; - //& console.log("[HabitDataService] Initialized."); } async fetchHabitValue(args: HabitDataArgs): Promise { - //& console.log(`[HabitDataService] fetchHabitValue called with:`, args); try { - validateFetchArgs(args); //~ Delegate validation - const safeTable = quoteSqlIdentifier(args.table); //~ Delegate quoting - const sql = `SELECT value FROM ${safeTable} WHERE habitKey = ? AND date = ?`; + validateFetchArgs(args); //~ Validate all args including column names + + //~ Quote all identifiers safely + const safeTable = quoteSqlIdentifier(args.table); + const safeHabitIdCol = quoteSqlIdentifier(args.habitIdCol); + const safeValueCol = quoteSqlIdentifier(args.valueCol); + const safeDateCol = quoteSqlIdentifier(args.dateCol); + + //^ Use specified columns. SELECT valueCol AS value to simplify return processing. + const sql = `SELECT ${safeValueCol} AS value FROM ${safeTable} WHERE ${safeHabitIdCol} = ? AND ${safeDateCol} = ?`; const params = [args.habitKey, args.date]; - //& console.log(`[HabitDataService] Executing fetch SQL: ${sql} PARAMS:`, params); + //? Use HabitRecord type directly because we aliased the column to 'value' const result = await this.dbService.getQuery(sql, params); - //& console.log(`[HabitDataService] Fetch result:`, result); + //? Check the 'value' property returned from the aliased select const value = result?.[0]?.value; - return (typeof value === 'number') ? value : 0; //? Default to 0 if no record or invalid value + return (typeof value === 'number') ? value : 0; + } catch (error) { console.error(`[HabitDataService] Error in fetchHabitValue for ${args.habitKey}:`, error); - throw error; //? Propagate error + throw error; } } async updateHabitValue(args: UpdateHabitDataArgs): Promise { - //& console.log(`[HabitDataService] updateHabitValue called with:`, args); try { - validateUpdateArgs(args); //~ Delegate validation - const safeTable = quoteSqlIdentifier(args.table); //~ Delegate quoting + validateUpdateArgs(args); //~ Validate all args + + //~ Quote all identifiers safely + const safeTable = quoteSqlIdentifier(args.table); + const safeHabitIdCol = quoteSqlIdentifier(args.habitIdCol); + const safeValueCol = quoteSqlIdentifier(args.valueCol); + const safeDateCol = quoteSqlIdentifier(args.dateCol); + + //^ Use specified columns in INSERT, ON CONFLICT target, and UPDATE SET const sql = ` - INSERT INTO ${safeTable} (habitKey, date, value) VALUES (?, ?, ?) - ON CONFLICT(habitKey, date) DO UPDATE SET value = excluded.value; + INSERT INTO ${safeTable} (${safeHabitIdCol}, ${safeDateCol}, ${safeValueCol}) VALUES (?, ?, ?) + ON CONFLICT(${safeHabitIdCol}, ${safeDateCol}) DO UPDATE SET ${safeValueCol} = excluded.${safeValueCol}; `; - //! Requires UNIQUE constraint on (habitKey, date) + //! Requires UNIQUE constraint on the specified (habitIdCol, dateCol) const params = [args.habitKey, args.date, args.newValue]; - //& console.log(`[HabitDataService] Executing update SQL: ${sql} PARAMS:`, params); await this.dbService.runQuery(sql, params); - //& console.log(`[HabitDataService] Update successful.`); + } catch (error) { console.error(`[HabitDataService] Error in updateHabitValue for ${args.habitKey}:`, error); - //? Log specific guidance for constraint errors if (error instanceof Error && error.message.includes("ON CONFLICT clause does not match")) { - console.error(`//! DB Constraint Error: Table '${args.table}' needs a UNIQUE index/key on (habitKey, date). Run: CREATE UNIQUE INDEX IF NOT EXISTS idx_habit_date ON ${quoteSqlIdentifier(args.table)} (habitKey, date);`); - throw new Error(`Missing UNIQUE constraint on (habitKey, date) in table ${args.table}. ${error.message}`); + //^ Provide more specific guidance mentioning the user-provided columns + console.error(`//! DB Constraint Error: Table '${args.table}' needs a UNIQUE index/key on (${args.habitIdCol}, ${args.dateCol}). Run: CREATE UNIQUE INDEX IF NOT EXISTS idx_habit_date ON ${quoteSqlIdentifier(args.table)} (${quoteSqlIdentifier(args.habitIdCol)}, ${quoteSqlIdentifier(args.dateCol)});`); + throw new Error(`Missing UNIQUE constraint on (${args.habitIdCol}, ${args.dateCol}) in table ${args.table}. ${error.message}`); } - throw error; //? Propagate other errors + throw error; } } } \ No newline at end of file diff --git a/src/webcomponents/HabitCounter/services/utils/validateHabitArgs.ts b/src/webcomponents/HabitCounter/services/utils/validateHabitArgs.ts index 5520993..5434cd4 100644 --- a/src/webcomponents/HabitCounter/services/utils/validateHabitArgs.ts +++ b/src/webcomponents/HabitCounter/services/utils/validateHabitArgs.ts @@ -1,15 +1,28 @@ import { HabitDataArgs, UpdateHabitDataArgs } from "../../HabitCounter.types"; +const IDENTIFIER_REGEX = /^[a-zA-Z0-9_]+$/; //? Basic check for valid SQL identifiers + /** Validates arguments required for fetching habit data. */ export function validateFetchArgs(args: HabitDataArgs): void { if (!args.table || !args.habitKey || !args.date) { - throw new Error(`Missing required arguments for fetch: table='${args.table}', habitKey='${args.habitKey}', date='${args.date}'`); + throw new Error(`Missing required identifying arguments: table='${args.table}', habitKey='${args.habitKey}', date='${args.date}'`); + } + //^ Validate column names + if (!args.habitIdCol || !IDENTIFIER_REGEX.test(args.habitIdCol)) { + throw new Error(`Invalid or missing habit ID column name: '${args.habitIdCol}'`); + } + if (!args.valueCol || !IDENTIFIER_REGEX.test(args.valueCol)) { + throw new Error(`Invalid or missing value column name: '${args.valueCol}'`); + } + if (!args.dateCol || !IDENTIFIER_REGEX.test(args.dateCol)) { + throw new Error(`Invalid or missing date column name: '${args.dateCol}'`); } } /** Validates arguments required for updating habit data. */ export function validateUpdateArgs(args: UpdateHabitDataArgs): void { - if (!args.table || !args.habitKey || !args.date || typeof args.newValue !== 'number' || args.newValue < 0) { - throw new Error(`Invalid or missing arguments for update: table='${args.table}', habitKey='${args.habitKey}', date='${args.date}', newValue='${args.newValue}'`); + validateFetchArgs(args); //? First validate base identifying args and columns + if (typeof args.newValue !== 'number' || args.newValue < 0) { + throw new Error(`Invalid newValue for update: '${args.newValue}' (must be a non-negative number)`); } } \ No newline at end of file diff --git a/src/webcomponents/HabitCounter/styles/applyHabitCounterStyles.ts b/src/webcomponents/HabitCounter/styles/applyHabitCounterStyles.ts index 4ad358f..5eb3fb8 100644 --- a/src/webcomponents/HabitCounter/styles/applyHabitCounterStyles.ts +++ b/src/webcomponents/HabitCounter/styles/applyHabitCounterStyles.ts @@ -2,7 +2,6 @@ import { getHabitCounterStyles } from "./getHabitCounterStyles"; /** Creates a style element and appends it to the shadow root. */ export function applyHabitCounterStyles(shadowRoot: ShadowRoot): void { - //& console.log("[HabitCounterStyles] applyHabitCounterStyles called."); if (!shadowRoot) { console.error("[HabitCounterStyles] Cannot apply styles: ShadowRoot is null."); return; diff --git a/src/webcomponents/HabitCounter/styles/getHabitCounterStyles.ts b/src/webcomponents/HabitCounter/styles/getHabitCounterStyles.ts index d002fb0..dd18047 100644 --- a/src/webcomponents/HabitCounter/styles/getHabitCounterStyles.ts +++ b/src/webcomponents/HabitCounter/styles/getHabitCounterStyles.ts @@ -1,6 +1,5 @@ /** Returns the CSS string for the component's Shadow DOM. */ export function getHabitCounterStyles(): string { - //& console.log("[HabitCounterStyles] getHabitCounterStyles called."); return ` :host { display: inline-flex; vertical-align: middle; } diff --git a/src/webcomponents/ji b/src/webcomponents/ji deleted file mode 100644 index e69de29..0000000