mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
abstracted the column logic
This commit is contained in:
parent
6ced6f6cca
commit
0d1bf53ea8
11 changed files with 102 additions and 79 deletions
|
|
@ -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<void> {
|
||||
//& 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
//& 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<vo
|
|||
const args = {
|
||||
table: instance.table,
|
||||
habitKey: instance.habitKey,
|
||||
date: calculateEffectiveDate(instance.initialDate), //~ Delegate date calculation
|
||||
date: calculateEffectiveDate(instance.initialDate),
|
||||
habitIdCol: instance.habitIdCol,
|
||||
valueCol: instance.valueCol,
|
||||
dateCol: instance.dateCol,
|
||||
};
|
||||
|
||||
try {
|
||||
const value = await instance.habitDataService.fetchHabitValue(args);
|
||||
//& console.log(`[LoadHabitData ${instance.habitKey}] Data loaded successfully: ${value}`);
|
||||
instance._updateDisplay(value);
|
||||
} catch (error) {
|
||||
console.error(`[LoadHabitData ${instance.habitKey}] Failed to load data:`, error);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ interface UpdateComponentInstance {
|
|||
habitDataService: HabitDataService | null;
|
||||
table: string;
|
||||
habitKey: string;
|
||||
initialDate: string; // Needed for effective date calculation
|
||||
currentValue: number; // The current value held by the component
|
||||
_updateDisplay: (value: number) => 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<void> {
|
||||
//& 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) {
|
||||
|
|
|
|||
|
|
@ -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]";
|
||||
|
|
|
|||
|
|
@ -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<number> {
|
||||
//& 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<HabitRecord>(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<void> {
|
||||
//& 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue