From ba1e1ccf869b818f7a7a897264dfea05563a7796 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 07 Jun 2026 20:10:20 +0000
Subject: [PATCH] =Major overhaul of Integrations. Playing around with adding fields to post types through Registrar from an integrations' class file.

---
 assets/js/concise/DataStore.js |  353 ++++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 267 insertions(+), 86 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 16c17ad..539c82b 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -45,13 +45,12 @@
 	 * @param {object|array} configs An object defining the store, or an array of objects defining the stores
 	 * @param {number} version the database version
 	 */
-	register(name, configs = [], version = 1.1) {
+	register(name, configs = [], version = 1.25) {
 		if (!Array.isArray(configs)) configs = [configs];
 		if (configs.length === 0) return;
-
 		if (!this.dbConfig.has(name)) {
 			this.dbConfig.set(name, {
-				dbName: `jvb_${name}`,
+				dbName: `${jvbBase.base}${name}`,
 				version: version,
 				stores: {},
 				_initialized: false
@@ -85,6 +84,8 @@
 					ignore: [],			//any filters to ignore when filtering store locally
 					required: null,
 
+					isAuth: false,
+
 					// Cache
 					TTL: 3600000, // 1 hour
 					useHttpCaching: true,
@@ -108,6 +109,7 @@
 
 			store.ignoreFilters = new Set([
 				... ['search', 'page', 'per_page', 'orderby', 'order'],
+				... ['context', 'source'],
 				... store.config.ignore
 			]);
 
@@ -124,6 +126,7 @@
 			}
 		});
 
+
 		// Initialize database asynchronously
 		this.initDB(name).catch(error => {
 			console.error(`Failed to initialize store "${name}":`, error);
@@ -144,8 +147,11 @@
 			// Data methods
 			fetch: () => this.fetch(name),
 			save: (item) => this.save(name, item),
+			saveMany: (items) => this.saveMany(name, items),
 			delete: (id) => this.delete(name, id),
+			deleteMany: (items) => this.deleteMany(name, items),
 			get: (id) => this.get(name, id),
+			getMany: (ids) => this.getMany(name, ids),
 			getAll: () => this.getAll(name),
 			getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
 			filterByIndex: (criteria) => this.filterByIndex(name, criteria),
@@ -468,13 +474,16 @@
 
 			let result;
 			tx.oncomplete = () => resolve(result);
-			tx.onerror = () => reject(tx.error);
+			tx.onerror = () => {
+				const error = tx.error || new Error('Transaction failed with unknown error');
+				reject(error);
+			};
 
 			// Call callback immediately to queue operations
 			try {
 				result = callback(objectStore, tx);
 			} catch (error) {
-				reject(error);
+				reject(error || new Error('Callback failed with unknown error'));
 			}
 		});
 	}
@@ -511,9 +520,10 @@
 			const cached = store.cache.get(cacheKey);
 
 			if (cached && this.isCacheValid(cached, store.config.TTL)) {
+				let items = cached.items.map(itemId => this.get(name, itemId));
 				this.notify(name, 'data-loaded', {
 					cached: true,
-					items: cached.items || []
+					items: items??[]
 				});
 				return cached;
 			}
@@ -534,11 +544,27 @@
 			const controller = new AbortController();
 			store.currentRequest = controller;
 
-			const response = await fetch(url, {
-				method: 'GET',
-				headers,
-				signal: controller.signal
-			});
+			let response;
+			if (store.isAuth) {
+				response = await window.auth.fetch(url, {
+					method: 'GET',
+					headers,
+					signal: controller.signal
+				});
+			} else {
+				response = await fetch(url, {
+					method: 'GET',
+					headers,
+					signal: controller.signal
+				});
+			}
+
+			if (!response.ok) {
+				// Access the error details from the response body
+				const errorBody = await response.text();
+				// Throw a new error with a descriptive message
+				throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
+			}
 
 			if (response.status === 304) {
 				// 304 means "Not Modified" - use cached data if available
@@ -573,7 +599,6 @@
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
-
 			const data = await response.json();
 
 			await this.processFetchedData(name, data, cacheKey, response);
@@ -586,11 +611,14 @@
 			return data;
 
 		} catch (error) {
-			if (error.name !== 'AbortError') {
-				console.error(`Fetch error for store "${name}":`, error);
+			const isAbortError = error?.name === 'AbortError';
+
+			if (!isAbortError) {
+				console.error(`Fetch error for store "${name}":`, error.message);
+				console.dir(error);
 				this.notify(name, 'fetch-error', { error });
+				throw error;
 			}
-			throw error;
 
 		} finally {
 			store.isFetching = false;
@@ -625,8 +653,8 @@
 	 */
 	async processFetchedData(name, data, cacheKey, response) {
 		const store = this.stores.get(name);
-		const items = data.items || [];
-		const changes = []; // Track all changes
+		const items = (data.items || []).filter(item => item && typeof item === 'object');
+		const changes = [];
 
 		// Batch process with single transaction
 		if (store.db && items.length > 0) {
@@ -784,8 +812,57 @@
 		return changeInfo.key;
 	}
 
+	/**
+	 * Save multiple items in a single transaction (batch write)
+	 * @param {string} name - Store name
+	 * @param {Array|Map} items - Array of items or Map of items to save
+	 * @returns {Promise<Array>} - Array of saved keys
+	 */
+	async saveMany(name, items) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Map to array if needed
+		const itemArray = items instanceof Map
+			? Array.from(items.values())
+			: Array.isArray(items) ? items : Object.values(items);
+
+		if (itemArray.length === 0) return [];
+
+		const changes = [];
+
+		// Process all items and update in-memory store
+		itemArray.forEach(item => {
+			const changeInfo = this._saveItem(name, item);
+			changes.push(changeInfo);
+		});
+
+		// Single transaction for all writes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			changes.forEach(changeInfo => {
+				objectStore.put(changeInfo.processed);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-saved', {
+			count: changes.length,
+			keys: changes.map(c => c.key)
+		});
+
+		return changes.map(c => c.key);
+	}
+
 	processForStorage(obj, validate = true, path = 'root') {
-		if (obj === null || obj === undefined) return { valid: true, data: obj };
+		if (obj === null) {
+			return { valid: true, data: null };
+		}
+		if (obj === undefined) {
+			if (validate) {
+				return { valid: false, error: `Undefined value at ${path}` };
+			}
+			return { valid: true, data: undefined };
+		}
 
 		const type = typeof obj;
 
@@ -844,9 +921,13 @@
 		if (type === 'object') {
 			const processed = {};
 			for (const [key, value] of Object.entries(obj)) {
+				if (value === undefined) continue;
 				const result = this.processForStorage(value, validate, `${path}.${key}`);
 				if (!result.valid) return result;
-				if (result.data !== undefined) processed[key] = result.data;
+				// Include null values, skip undefined
+				if (result.data !== undefined || value === null) {
+					processed[key] = result.data;
+				}
 			}
 			return { valid: true, data: processed };
 		}
@@ -871,11 +952,78 @@
 		this.notify(name, 'item-deleted', { id });
 	}
 
+	/**
+	 * Delete multiple items in a single transaction (batch delete)
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to delete
+	 * @returns {Promise<Array>} - Array of deleted IDs
+	 */
+	async deleteMany(name, ids) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		// Convert Set to array if needed
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		// Update in-memory store
+		idArray.forEach(id => {
+			store.data.delete(id);
+		});
+
+		// Single transaction for all deletes
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			idArray.forEach(id => {
+				objectStore.delete(id);
+			});
+		});
+
+		// Notify once for batch
+		this.notify(name, 'items-deleted', {
+			count: idArray.length,
+			ids: idArray
+		});
+
+		return idArray;
+	}
+
 	get(name, id) {
 		const store = this.stores.get(name);
 		return store.data.get(id);
 	}
 
+	/**
+	 * Get multiple items by IDs in a single call
+	 * @param {string} name - Store name
+	 * @param {Array|Set} ids - Array or Set of IDs to retrieve
+	 * @param {boolean} skipMissing - If true, omit missing items; if false, include null for missing
+	 * @returns {Array} - Array of items (in same order as IDs)
+	 */
+	getMany(name, ids, skipMissing = true) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		const idArray = ids instanceof Set
+			? Array.from(ids)
+			: Array.isArray(ids) ? ids : Object.keys(ids);
+
+		if (idArray.length === 0) return [];
+
+		if (skipMissing) {
+			return idArray.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		// Preserve order, include null for missing
+		return idArray.map(id => store.data.get(id) ?? null);
+	}
+
 	getAll(name) {
 		const store = this.stores.get(name);
 		return Array.from(store.data.values());
@@ -894,6 +1042,7 @@
 		if (!store) return [];
 
 		return Array.from(store.data.values()).filter(item => {
+			if (!item || typeof item !== 'object') return false;
 			return Object.entries(criteria).every(([key, value]) => {
 				const accepted = Array.isArray(value) ? value : [value];
 				return accepted.includes(item[key]);
@@ -946,40 +1095,63 @@
 
 		// First check if we have cached results for exact filters
 		if (cacheEntry?.items) {
-			return this.applyOrdering(
-				cacheEntry.items.reduce((acc, id) => {
-					const item = store.data.get(id);
-					if (item) acc.push(item);
-					return acc;
-				}, []),
-				store
-			);
+			const items = cacheEntry.items.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+			return this.applyOrdering(items, store);
 		}
 
 		const allItems = Array.from(store.data.values());
+
 		const searchQuery = store.filters.search?.toLowerCase().trim() || '';
 
-		const filtered = allItems.filter(item => {
-			// Apply all non-ignored filters
-			for (const [key, value] of Object.entries(store.filters)) {
-				if (store.ignoreFilters.has(key)) continue;
-				if (value === null || value === undefined || value === '') continue;
-				if (value === 'all') continue;
+		const filterPredicates = [];
 
-				// Comma-separated values
-				if (typeof value === 'string' && value.includes(',')) {
-					const accepted = value.split(',').map(v => v.trim());
-					if (!accepted.includes(String(item[key]))) return false;
-					continue;
+		// Handle taxonomy filters separately
+		if (store.filters.taxonomy && typeof store.filters.taxonomy === 'object') {
+			Object.entries(store.filters.taxonomy).forEach(([taxonomy, termIds]) => {
+				const acceptedTermIds = Array.isArray(termIds) ? termIds : [termIds];
+
+				filterPredicates.push(item => {
+					if (!item.taxonomies || !item.taxonomies[taxonomy]) {
+						return false;
+					}
+					const itemTermIds = Object.keys(item.taxonomies[taxonomy]).map(id => parseInt(id));
+					const matches = acceptedTermIds.some(termId => itemTermIds.includes(parseInt(termId)));
+					return matches;
+				});
+			});
+		}
+
+		// Handle other filters
+		for (const [key, value] of Object.entries(store.filters)) {
+			if (key === 'taxonomy') {
+				if (typeof value === 'string' && !value.includes(',')) {
+					filterPredicates.push(item => item.taxonomy === value);
 				}
-
-				if (String(item[key]) !== String(value)) return false;
+				continue;
 			}
+			if (store.ignoreFilters.has(key)) {
+				continue;
+			}
+			if (value === null || value === undefined || value === '') continue;
+			if (value === 'all') continue;
 
-			// Apply search if present
+			if (typeof value === 'string' && value.includes(',')) {
+				const accepted = value.split(',').map(v => v.trim());
+				filterPredicates.push(item => accepted.includes(String(item[key])));
+			} else {
+				filterPredicates.push(item => String(item[key]) === String(value));
+			}
+		}
+
+		const filtered = allItems.filter(item => {
+			for (const predicate of filterPredicates) {
+				if (!predicate(item)) return false;
+			}
 			return !(searchQuery && !this.searchObject(item, searchQuery));
-
-
 		});
 
 		return this.applyOrdering(filtered, store);
@@ -989,39 +1161,54 @@
 		if (!Array.isArray(items)) items = Array.from(items);
 		if (items.length === 0) return items;
 
-		if (store.filters.orderby || store.filters.order) {
-			const orderby = store.filters.orderby || 'date';
-			const order = (store.filters.order || 'desc').toLowerCase();
+		const orderby = store.filters.orderby || 'date';
+		const order = (store.filters.order || 'desc').toLowerCase();
 
-			items.sort((a, b) => {
-				let aVal, bVal;
-
-				switch (orderby) {
-					case 'alphabetical':
-					case 'title':
-						aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
-						bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
-						break;
-					case 'modified':
-						aVal = new Date(a.modified || 0);
-						bVal = new Date(b.modified || 0);
-						break;
-					case 'date':
-					default:
-						aVal = new Date(a.date || 0);
-						bVal = new Date(b.date || 0);
-				}
-
-				if (aVal < bVal) return order === 'asc' ? -1 : 1;
-				if (aVal > bVal) return order === 'asc' ? 1 : -1;
-				return 0;
-			});
+		// Handle random ordering
+		if (['random', 'rand'].includes(orderby) || ['random', 'rand'].includes(order)) {
+			return this.shuffle(items);
 		}
+
+		items.sort((a, b) => {
+			let aVal, bVal;
+
+			switch (orderby) {
+				case 'alphabetical':
+				case 'title':
+					aVal = (a.title || a.name || '').toLowerCase();
+					bVal = (b.title || b.name || '').toLowerCase();
+					break;
+				case 'modified':
+					aVal = new Date(a.modified || a.date || 0);
+					bVal = new Date(b.modified || b.date || 0);
+					break;
+				case 'date':
+				default:
+					aVal = new Date(a.date || a.modified || 0);
+					bVal = new Date(b.date || b.modified || 0);
+			}
+
+			if (aVal < bVal) return order === 'asc' ? -1 : 1;
+			if (aVal > bVal) return order === 'asc' ? 1 : -1;
+			return 0;
+		});
+
 		return items;
 	}
 
+	shuffle(items) {
+		const array = items.slice();
+		for (let i = array.length - 1; i > 0; i--) {
+			const j = Math.floor(Math.random() * (i + 1));
+			[array[i], array[j]] = [array[j], array[i]];
+		}
+		return array;
+	}
+
 	searchObject(obj, search) {
-		if (!obj || typeof obj !== 'object') return false;
+		if (!obj || typeof obj !== 'object') {
+			return typeof obj === 'string' && obj.toLowerCase().includes(search);
+		}
 
 		for (const value of Object.values(obj)) {
 			if (value === null || value === undefined) continue;
@@ -1068,23 +1255,22 @@
 				store.filters[key] = value;
 			}
 		});
-
 		this.notify(name, 'filters-changed', {
 			oldFilters,
 			filters: store.filters,
 			updates
 		});
 
-		this.notify(name, 'data-loaded', {
-			cached: true,
-			items: this.getFiltered(name)
-		});
-
 		const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
+
 		if (store.config.endpoint && shouldFetch) {
 			await this.fetch(name);
-		} else if (store.config.endpoint) {
-			this.notify(name, 'data-loaded');
+		} else {
+			const filtered = this.getFiltered(name);
+			this.notify(name, 'data-loaded', {
+				cached: true,
+				items: filtered
+			});
 		}
 	}
 
@@ -1104,14 +1290,9 @@
 		}
 
 		if (store.lastResponse.has_more === false) {
-			// Check if new filters are a subset of what we have
-			const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
-				if (store.ignoreFilters.has(key)) return true;
-				if (key === 'page') return true; // Handle pagination locally
-				return true; // We have all data, can filter locally
-			});
-
-			if (isSubsetFilter) return false;
+			if (this.hasCompleteData(store, store.filters)) {
+				return false;
+			}
 		}
 
 		if ('page' in updates) {

--
Gitblit v1.10.0