fix indexdb

This commit is contained in:
chark1es 2025-03-01 17:32:13 -08:00
parent 9d7058d533
commit a1451fa534
5 changed files with 411 additions and 271 deletions

View file

@ -1,7 +0,0 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#221489",
"titleBar.activeBackground": "#301DC0",
"titleBar.activeForeground": "#F9F9FE"
}
}

View file

@ -1,8 +1,12 @@
import { Authentication } from '../pocketbase/Authentication';
import { DataSyncService } from './DataSyncService';
import { DexieService } from './DexieService';
import { Collections } from '../../schemas/pocketbase/schema';
import { SendLog } from '../pocketbase/SendLog';
import { Authentication } from "../pocketbase/Authentication";
import { DataSyncService } from "./DataSyncService";
import { DexieService } from "./DexieService";
import { Collections } from "../../schemas/pocketbase/schema";
import { SendLog } from "../pocketbase/SendLog";
// Check if we're in a browser environment
const isBrowser =
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
/**
* Service to handle data synchronization during authentication flows
@ -27,7 +31,7 @@ export class AuthSyncService {
Collections.OFFICERS,
Collections.REIMBURSEMENTS,
Collections.RECEIPTS,
Collections.SPONSORS
Collections.SPONSORS,
];
private constructor() {
@ -36,8 +40,10 @@ export class AuthSyncService {
this.dexieService = DexieService.getInstance();
this.logger = SendLog.getInstance();
// Listen for auth state changes
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
// Listen for auth state changes only in browser
if (isBrowser) {
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
}
}
/**
@ -54,6 +60,8 @@ export class AuthSyncService {
* Handle authentication state changes
*/
private async handleAuthStateChange(isAuthenticated: boolean): Promise<void> {
if (!isBrowser) return;
if (isAuthenticated) {
// User just logged in
await this.handleLogin();
@ -67,8 +75,10 @@ export class AuthSyncService {
* Handle login by syncing user data
*/
public async handleLogin(): Promise<boolean> {
if (!isBrowser) return true;
if (this.isSyncing) {
console.log('Sync already in progress, queueing login sync');
console.log("Sync already in progress, queueing login sync");
if (this.syncPromise) {
this.syncPromise = this.syncPromise.then(() => this.performLoginSync());
} else {
@ -78,15 +88,19 @@ export class AuthSyncService {
}
this.syncPromise = this.performLoginSync();
return this.syncPromise.then(() => Object.keys(this.syncErrors).length === 0);
return this.syncPromise.then(
() => Object.keys(this.syncErrors).length === 0,
);
}
/**
* Perform the actual login sync
*/
private async performLoginSync(): Promise<void> {
if (!isBrowser) return;
if (!this.auth.isAuthenticated()) {
console.log('Not authenticated, skipping login sync');
console.log("Not authenticated, skipping login sync");
return;
}
@ -94,19 +108,22 @@ export class AuthSyncService {
this.syncErrors = {};
try {
console.log('Starting login sync process...');
console.log("Starting login sync process...");
// Display sync notification if in browser environment
this.showSyncNotification('Syncing your data...');
this.showSyncNotification("Syncing your data...");
// Sync user-specific data first
const userId = this.auth.getUserId();
if (userId) {
// First sync the current user's data
await this.dataSync.syncCollection(Collections.USERS, `id = "${userId}"`);
await this.dataSync.syncCollection(
Collections.USERS,
`id = "${userId}"`,
);
// Log the sync operation
console.log('User data synchronized on login');
console.log("User data synchronized on login");
}
// Sync all collections in parallel with conflict resolution
@ -119,32 +136,34 @@ export class AuthSyncService {
console.error(`Error syncing ${collection}:`, error);
this.syncErrors[collection] = error as Error;
}
})
}),
);
// Verify sync was successful
const syncVerification = await this.verifySyncSuccess();
if (syncVerification.success) {
console.log('Login sync completed successfully');
this.showSyncNotification('Data sync complete!', 'success');
console.log("Login sync completed successfully");
this.showSyncNotification("Data sync complete!", "success");
} else {
console.warn('Login sync completed with issues:', syncVerification.errors);
this.showSyncNotification('Some data could not be synced', 'warning');
console.warn(
"Login sync completed with issues:",
syncVerification.errors,
);
this.showSyncNotification("Some data could not be synced", "warning");
}
} catch (error) {
console.error('Error during login sync:', error);
this.showSyncNotification('Failed to sync data', 'error');
console.error("Error during login sync:", error);
this.showSyncNotification("Failed to sync data", "error");
} finally {
this.isSyncing = false;
// Process any queued sync operations
if (this.syncQueue.length > 0) {
const nextSync = this.syncQueue.shift();
if (nextSync === 'login') {
if (nextSync === "login") {
this.handleLogin();
} else if (nextSync === 'logout') {
} else if (nextSync === "logout") {
this.handleLogout();
}
}
@ -155,16 +174,18 @@ export class AuthSyncService {
* Handle logout by clearing user data
*/
public async handleLogout(): Promise<boolean> {
if (!isBrowser) return true;
if (this.isSyncing) {
console.log('Sync already in progress, queueing logout cleanup');
this.syncQueue.push('logout');
console.log("Sync already in progress, queueing logout cleanup");
this.syncQueue.push("logout");
return true;
}
this.isSyncing = true;
try {
console.log('Starting logout cleanup process...');
console.log("Starting logout cleanup process...");
// Ensure any pending changes are synced before logout
await this.syncPendingChanges();
@ -172,10 +193,10 @@ export class AuthSyncService {
// Clear all data from IndexedDB
await this.dexieService.clearAllData();
console.log('Logout cleanup completed successfully');
console.log("Logout cleanup completed successfully");
return true;
} catch (error) {
console.error('Error during logout cleanup:', error);
console.error("Error during logout cleanup:", error);
return false;
} finally {
this.isSyncing = false;
@ -183,9 +204,9 @@ export class AuthSyncService {
// Process any queued sync operations
if (this.syncQueue.length > 0) {
const nextSync = this.syncQueue.shift();
if (nextSync === 'login') {
if (nextSync === "login") {
this.handleLogin();
} else if (nextSync === 'logout') {
} else if (nextSync === "logout") {
this.handleLogout();
}
}
@ -196,16 +217,23 @@ export class AuthSyncService {
* Sync any pending changes before logout
*/
private async syncPendingChanges(): Promise<void> {
if (!isBrowser) return;
// This would be implemented if we had offline capabilities
// For now, we just log that we would sync pending changes
console.log('Checking for pending changes to sync before logout...');
console.log("Checking for pending changes to sync before logout...");
// In a real implementation, this would sync any offline changes
}
/**
* Verify that sync was successful by checking data in IndexedDB
*/
private async verifySyncSuccess(): Promise<{ success: boolean; errors: Record<string, string> }> {
private async verifySyncSuccess(): Promise<{
success: boolean;
errors: Record<string, string>;
}> {
if (!isBrowser) return { success: true, errors: {} };
const errors: Record<string, string> = {};
// Check each collection that had errors
@ -217,30 +245,39 @@ export class AuthSyncService {
const userId = this.auth.getUserId();
if (userId) {
try {
const user = await this.dataSync.getItem(Collections.USERS, userId, false);
const user = await this.dataSync.getItem(
Collections.USERS,
userId,
false,
);
if (!user) {
errors['user_verification'] = 'User data not found in IndexedDB after sync';
errors["user_verification"] =
"User data not found in IndexedDB after sync";
}
} catch (error) {
errors['user_verification'] = `Error verifying user data: ${(error as Error).message}`;
errors["user_verification"] =
`Error verifying user data: ${(error as Error).message}`;
}
}
return {
success: Object.keys(errors).length === 0,
errors
errors,
};
}
/**
* Show a notification to the user about sync status
*/
private showSyncNotification(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void {
private showSyncNotification(
message: string,
type: "info" | "success" | "warning" | "error" = "info",
): void {
// Only run in browser environment
if (typeof window === 'undefined') return;
if (!isBrowser) return;
// Check if toast function exists (from react-hot-toast or similar)
if (typeof window.toast === 'function') {
if (typeof window.toast === "function") {
window.toast(message, { type });
} else {
// Fallback to console
@ -253,8 +290,8 @@ export class AuthSyncService {
*/
public async forceSyncAll(): Promise<boolean> {
if (this.isSyncing) {
console.log('Sync already in progress, queueing full sync');
this.syncQueue.push('login'); // Reuse login sync logic
console.log("Sync already in progress, queueing full sync");
this.syncQueue.push("login"); // Reuse login sync logic
return true;
}
@ -279,6 +316,9 @@ export class AuthSyncService {
// Add toast type to window for TypeScript
declare global {
interface Window {
toast?: (message: string, options?: { type: 'info' | 'success' | 'warning' | 'error' }) => void;
toast?: (
message: string,
options?: { type: "info" | "success" | "warning" | "error" },
) => void;
}
}

View file

@ -1,16 +1,20 @@
import { DexieService } from './DexieService';
import { Get } from '../pocketbase/Get';
import { Update } from '../pocketbase/Update';
import { Authentication } from '../pocketbase/Authentication';
import { Collections, type BaseRecord } from '../../schemas/pocketbase/schema';
import type Dexie from 'dexie';
import { DexieService } from "./DexieService";
import { Get } from "../pocketbase/Get";
import { Update } from "../pocketbase/Update";
import { Authentication } from "../pocketbase/Authentication";
import { Collections, type BaseRecord } from "../../schemas/pocketbase/schema";
import type Dexie from "dexie";
// Check if we're in a browser environment
const isBrowser =
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
// Interface for tracking offline changes
interface OfflineChange {
id: string;
collection: string;
recordId: string;
operation: 'create' | 'update' | 'delete';
operation: "create" | "update" | "delete";
data?: any;
timestamp: number;
synced: boolean;
@ -33,13 +37,13 @@ export class DataSyncService {
this.update = Update.getInstance();
this.auth = Authentication.getInstance();
// Initialize offline changes table
this.initOfflineChangesTable();
// Initialize offline changes table only in browser
if (isBrowser) {
this.initOfflineChangesTable();
// Check for network status
if (typeof window !== 'undefined') {
window.addEventListener('online', this.handleOnline.bind(this));
window.addEventListener('offline', this.handleOffline.bind(this));
// Check for network status
window.addEventListener("online", this.handleOnline.bind(this));
window.addEventListener("offline", this.handleOffline.bind(this));
this.offlineMode = !navigator.onLine;
}
}
@ -55,16 +59,21 @@ export class DataSyncService {
* Initialize the offline changes table
*/
private initOfflineChangesTable(): void {
if (!isBrowser) return;
try {
const db = this.dexieService.getDB();
// Check if the table exists in the schema
if ('offlineChanges' in db) {
this.offlineChangesTable = db.offlineChanges as Dexie.Table<OfflineChange, string>;
if ("offlineChanges" in db) {
this.offlineChangesTable = db.offlineChanges as Dexie.Table<
OfflineChange,
string
>;
} else {
console.warn('Offline changes table not found in schema');
console.warn("Offline changes table not found in schema");
}
} catch (error) {
console.error('Error initializing offline changes table:', error);
console.error("Error initializing offline changes table:", error);
}
}
@ -72,7 +81,9 @@ export class DataSyncService {
* Handle device coming online
*/
private async handleOnline(): Promise<void> {
console.log('Device is online, syncing pending changes...');
if (!isBrowser) return;
console.log("Device is online, syncing pending changes...");
this.offlineMode = false;
await this.syncOfflineChanges();
}
@ -81,7 +92,9 @@ export class DataSyncService {
* Handle device going offline
*/
private handleOffline(): void {
console.log('Device is offline, enabling offline mode...');
if (!isBrowser) return;
console.log("Device is offline, enabling offline mode...");
this.offlineMode = true;
}
@ -90,10 +103,16 @@ export class DataSyncService {
*/
public async syncCollection<T extends BaseRecord>(
collection: string,
filter: string = '',
sort: string = '-created',
expand: Record<string, any> | string[] | string = {}
filter: string = "",
sort: string = "-created",
expand: Record<string, any> | string[] | string = {},
): Promise<T[]> {
// Skip in non-browser environments
if (!isBrowser) {
console.log(`Skipping sync for ${collection} in non-browser environment`);
return [];
}
// Prevent multiple syncs of the same collection at the same time
if (this.syncInProgress[collection]) {
console.log(`Sync already in progress for ${collection}`);
@ -114,7 +133,7 @@ export class DataSyncService {
console.log(`Device is offline, using cached data for ${collection}`);
const db = this.dexieService.getDB();
const table = this.getTableForCollection(collection);
return table ? (await table.toArray() as T[]) : [];
return table ? ((await table.toArray()) as T[]) : [];
}
console.log(`Syncing ${collection}...`);
@ -123,13 +142,13 @@ export class DataSyncService {
let normalizedExpand: string[] | undefined;
if (expand) {
if (typeof expand === 'string') {
if (typeof expand === "string") {
// If expand is a string, convert it to an array
normalizedExpand = [expand];
} else if (Array.isArray(expand)) {
// If expand is already an array, use it as is
normalizedExpand = expand;
} else if (typeof expand === 'object') {
} else if (typeof expand === "object") {
// If expand is an object, extract the keys
normalizedExpand = Object.keys(expand);
}
@ -137,7 +156,7 @@ export class DataSyncService {
// Get data from PocketBase
const items = await this.get.getAll<T>(collection, filter, sort, {
expand: normalizedExpand
expand: normalizedExpand,
});
console.log(`Fetched ${items.length} items from ${collection}`);
@ -152,20 +171,28 @@ export class DataSyncService {
// Get existing items to handle conflicts
const existingItems = await table.toArray();
const existingItemsMap = new Map(existingItems.map(item => [item.id, item]));
const existingItemsMap = new Map(
existingItems.map((item) => [item.id, item]),
);
// Handle conflicts and merge changes
const itemsToStore = await Promise.all(items.map(async (item) => {
const existingItem = existingItemsMap.get(item.id);
const itemsToStore = await Promise.all(
items.map(async (item) => {
const existingItem = existingItemsMap.get(item.id);
if (existingItem) {
// Check for conflicts (local changes vs server changes)
const resolvedItem = await this.resolveConflict(collection, existingItem, item);
return resolvedItem;
}
if (existingItem) {
// Check for conflicts (local changes vs server changes)
const resolvedItem = await this.resolveConflict(
collection,
existingItem,
item,
);
return resolvedItem;
}
return item;
}));
return item;
}),
);
// Store in IndexedDB
await table.bulkPut(itemsToStore);
@ -188,20 +215,25 @@ export class DataSyncService {
private async resolveConflict<T extends BaseRecord>(
collection: string,
localItem: T,
serverItem: T
serverItem: T,
): Promise<T> {
// Check if there are pending offline changes for this item
const pendingChanges = await this.getPendingChangesForRecord(collection, localItem.id);
const pendingChanges = await this.getPendingChangesForRecord(
collection,
localItem.id,
);
if (pendingChanges.length > 0) {
console.log(`Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`);
console.log(
`Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`,
);
// Server-wins strategy by default, but preserve local changes that haven't been synced
const mergedItem = { ...serverItem };
// Apply pending changes on top of server data
for (const change of pendingChanges) {
if (change.operation === 'update' && change.data) {
if (change.operation === "update" && change.data) {
// Apply each field change individually
Object.entries(change.data).forEach(([key, value]) => {
(mergedItem as any)[key] = value;
@ -219,17 +251,23 @@ export class DataSyncService {
/**
* Get pending changes for a specific record
*/
private async getPendingChangesForRecord(collection: string, recordId: string): Promise<OfflineChange[]> {
private async getPendingChangesForRecord(
collection: string,
recordId: string,
): Promise<OfflineChange[]> {
if (!this.offlineChangesTable) return [];
try {
return await this.offlineChangesTable
.where('collection')
.where("collection")
.equals(collection)
.and(item => item.recordId === recordId && !item.synced)
.and((item) => item.recordId === recordId && !item.synced)
.toArray();
} catch (error) {
console.error(`Error getting pending changes for ${collection}:${recordId}:`, error);
console.error(
`Error getting pending changes for ${collection}:${recordId}:`,
error,
);
return [];
}
}
@ -243,26 +281,29 @@ export class DataSyncService {
try {
// Get all unsynced changes
const pendingChanges = await this.offlineChangesTable
.where('synced')
.where("synced")
.equals(0) // Use 0 instead of false for indexable type
.toArray();
if (pendingChanges.length === 0) {
console.log('No pending offline changes to sync');
console.log("No pending offline changes to sync");
return true;
}
console.log(`Syncing ${pendingChanges.length} offline changes...`);
// Group changes by collection for more efficient processing
const changesByCollection = pendingChanges.reduce((groups, change) => {
const key = change.collection;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(change);
return groups;
}, {} as Record<string, OfflineChange[]>);
const changesByCollection = pendingChanges.reduce(
(groups, change) => {
const key = change.collection;
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(change);
return groups;
},
{} as Record<string, OfflineChange[]>,
);
// Process each collection's changes
for (const [collection, changes] of Object.entries(changesByCollection)) {
@ -272,13 +313,17 @@ export class DataSyncService {
// Then apply each change
for (const change of changes) {
try {
if (change.operation === 'update' && change.data) {
await this.update.updateFields(collection, change.recordId, change.data);
if (change.operation === "update" && change.data) {
await this.update.updateFields(
collection,
change.recordId,
change.data,
);
// Mark as synced
await this.offlineChangesTable.update(change.id, {
synced: true,
syncAttempts: change.syncAttempts + 1
syncAttempts: change.syncAttempts + 1,
});
}
// Add support for create and delete operations as needed
@ -287,7 +332,7 @@ export class DataSyncService {
// Increment sync attempts
await this.offlineChangesTable.update(change.id, {
syncAttempts: change.syncAttempts + 1
syncAttempts: change.syncAttempts + 1,
});
}
}
@ -298,7 +343,7 @@ export class DataSyncService {
return true;
} catch (error) {
console.error('Error syncing offline changes:', error);
console.error("Error syncing offline changes:", error);
return false;
}
}
@ -309,35 +354,40 @@ export class DataSyncService {
public async recordOfflineChange(
collection: string,
recordId: string,
operation: 'create' | 'update' | 'delete',
data?: any
operation: "create" | "update" | "delete",
data?: any,
): Promise<string | null> {
if (!this.offlineChangesTable) return null;
try {
const change: Omit<OfflineChange, 'id'> = {
const change: Omit<OfflineChange, "id"> = {
collection,
recordId,
operation,
data,
timestamp: Date.now(),
synced: false,
syncAttempts: 0
syncAttempts: 0,
};
const id = await this.offlineChangesTable.add(change as OfflineChange);
console.log(`Recorded offline change: ${operation} on ${collection}:${recordId}`);
console.log(
`Recorded offline change: ${operation} on ${collection}:${recordId}`,
);
// Try to sync immediately if we're online
if (!this.offlineMode) {
this.syncOfflineChanges().catch(err => {
console.error('Error syncing after recording change:', err);
this.syncOfflineChanges().catch((err) => {
console.error("Error syncing after recording change:", err);
});
}
return id;
} catch (error) {
console.error(`Error recording offline change for ${collection}:${recordId}:`, error);
console.error(
`Error recording offline change for ${collection}:${recordId}:`,
error,
);
return null;
}
}
@ -348,9 +398,9 @@ export class DataSyncService {
public async getData<T extends BaseRecord>(
collection: string,
forceSync: boolean = false,
filter: string = '',
sort: string = '-created',
expand: Record<string, any> | string[] | string = {}
filter: string = "",
sort: string = "-created",
expand: Record<string, any> | string[] | string = {},
): Promise<T[]> {
const db = this.dexieService.getDB();
const table = this.getTableForCollection(collection);
@ -365,7 +415,7 @@ export class DataSyncService {
const now = Date.now();
const syncThreshold = 5 * 60 * 1000; // 5 minutes
if (!this.offlineMode && (forceSync || (now - lastSync > syncThreshold))) {
if (!this.offlineMode && (forceSync || now - lastSync > syncThreshold)) {
try {
await this.syncCollection<T>(collection, filter, sort, expand);
} catch (error) {
@ -382,12 +432,12 @@ export class DataSyncService {
// and apply it properly. This is just a basic example.
data = data.filter((item: any) => {
// Split filter by logical operators
const conditions = filter.split(' && ');
return conditions.every(condition => {
const conditions = filter.split(" && ");
return conditions.every((condition) => {
// Parse condition (very basic implementation)
if (condition.includes('=')) {
const [field, value] = condition.split('=');
const cleanValue = value.replace(/"/g, '');
if (condition.includes("=")) {
const [field, value] = condition.split("=");
const cleanValue = value.replace(/"/g, "");
return item[field] === cleanValue;
}
return true;
@ -397,7 +447,7 @@ export class DataSyncService {
// Apply sort if provided
if (sort) {
const isDesc = sort.startsWith('-');
const isDesc = sort.startsWith("-");
const field = isDesc ? sort.substring(1) : sort;
data.sort((a: any, b: any) => {
@ -413,7 +463,11 @@ export class DataSyncService {
/**
* Get a single item by ID
*/
public async getItem<T extends BaseRecord>(collection: string, id: string, forceSync: boolean = false): Promise<T | undefined> {
public async getItem<T extends BaseRecord>(
collection: string,
id: string,
forceSync: boolean = false,
): Promise<T | undefined> {
const db = this.dexieService.getDB();
const table = this.getTableForCollection(collection);
@ -423,7 +477,7 @@ export class DataSyncService {
}
// Try to get from IndexedDB first
let item = await table.get(id) as T | undefined;
let item = (await table.get(id)) as T | undefined;
// If not found or force sync, try to get from PocketBase
if ((!item || forceSync) && !this.offlineMode) {
@ -447,7 +501,7 @@ export class DataSyncService {
public async updateItem<T extends BaseRecord>(
collection: string,
id: string,
data: Partial<T>
data: Partial<T>,
): Promise<T | undefined> {
const table = this.getTableForCollection(collection);
@ -457,19 +511,23 @@ export class DataSyncService {
}
// Get the current item
const currentItem = await table.get(id) as T | undefined;
const currentItem = (await table.get(id)) as T | undefined;
if (!currentItem) {
console.error(`Item ${id} not found in ${collection}`);
return undefined;
}
// Update the item in IndexedDB
const updatedItem = { ...currentItem, ...data, updated: new Date().toISOString() };
const updatedItem = {
...currentItem,
...data,
updated: new Date().toISOString(),
};
await table.put(updatedItem);
// If offline, record the change for later sync
if (this.offlineMode) {
await this.recordOfflineChange(collection, id, 'update', data);
await this.recordOfflineChange(collection, id, "update", data);
return updatedItem;
}
@ -481,7 +539,7 @@ export class DataSyncService {
console.error(`Error updating ${collection} item ${id}:`, error);
// Record as offline change to retry later
await this.recordOfflineChange(collection, id, 'update', data);
await this.recordOfflineChange(collection, id, "update", data);
return updatedItem;
}
}
@ -503,7 +561,9 @@ export class DataSyncService {
/**
* Get the appropriate Dexie table for a collection
*/
private getTableForCollection(collection: string): Dexie.Table<any, string> | null {
private getTableForCollection(
collection: string,
): Dexie.Table<any, string> | null {
const db = this.dexieService.getDB();
switch (collection) {

View file

@ -1,4 +1,4 @@
import Dexie from 'dexie';
import Dexie from "dexie";
import type {
User,
Event,
@ -7,15 +7,19 @@ import type {
Officer,
Reimbursement,
Receipt,
Sponsor
} from '../../schemas/pocketbase/schema';
Sponsor,
} from "../../schemas/pocketbase/schema";
// Check if we're in a browser environment
const isBrowser =
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
// Interface for tracking offline changes
interface OfflineChange {
id: string;
collection: string;
recordId: string;
operation: 'create' | 'update' | 'delete';
operation: "create" | "update" | "delete";
data?: any;
timestamp: number;
synced: boolean;
@ -34,34 +38,44 @@ export class DashboardDatabase extends Dexie {
offlineChanges!: Dexie.Table<OfflineChange, string>;
// Store last sync timestamps
syncInfo!: Dexie.Table<{id: string, collection: string, lastSync: number}, string>;
syncInfo!: Dexie.Table<
{ id: string; collection: string; lastSync: number },
string
>;
constructor() {
super('IEEEDashboardDB');
super("IEEEDashboardDB");
this.version(1).stores({
users: 'id, email, name',
events: 'id, event_name, event_code, start_date, end_date, published',
eventRequests: 'id, name, status, requested_user, created, updated',
logs: 'id, user, type, created',
officers: 'id, user, role, type',
reimbursements: 'id, title, status, submitted_by, created',
receipts: 'id, created_by, date',
sponsors: 'id, user, company',
syncInfo: 'id, collection, lastSync'
users: "id, email, name",
events: "id, event_name, event_code, start_date, end_date, published",
eventRequests: "id, name, status, requested_user, created, updated",
logs: "id, user, type, created",
officers: "id, user, role, type",
reimbursements: "id, title, status, submitted_by, created",
receipts: "id, created_by, date",
sponsors: "id, user, company",
syncInfo: "id, collection, lastSync",
});
// Add version 2 with offlineChanges table
this.version(2).stores({
offlineChanges: 'id, collection, recordId, operation, timestamp, synced, syncAttempts'
offlineChanges:
"id, collection, recordId, operation, timestamp, synced, syncAttempts",
});
}
// Initialize the database with default values
async initialize() {
const collections = [
'users', 'events', 'event_request', 'logs',
'officers', 'reimbursement', 'receipts', 'sponsors'
"users",
"events",
"event_request",
"logs",
"officers",
"reimbursement",
"receipts",
"sponsors",
];
for (const collection of collections) {
@ -70,21 +84,36 @@ export class DashboardDatabase extends Dexie {
await this.syncInfo.put({
id: collection,
collection,
lastSync: 0
lastSync: 0,
});
}
}
}
}
// Mock database for server-side rendering
class MockDashboardDatabase {
// Implement empty methods that won't fail during SSR
async initialize() {
// Do nothing
}
}
// Singleton pattern
export class DexieService {
private static instance: DexieService;
private db: DashboardDatabase;
private db: DashboardDatabase | MockDashboardDatabase;
private constructor() {
this.db = new DashboardDatabase();
this.db.initialize();
if (isBrowser) {
// Only initialize Dexie in browser environments
this.db = new DashboardDatabase();
this.db.initialize();
} else {
// Use a mock database in non-browser environments
console.log("Running in Node.js environment, using mock database");
this.db = new MockDashboardDatabase() as any;
}
}
public static getInstance(): DexieService {
@ -96,40 +125,58 @@ export class DexieService {
// Get the database instance
public getDB(): DashboardDatabase {
return this.db;
if (!isBrowser) {
console.warn(
"Attempting to access IndexedDB in a non-browser environment",
);
}
return this.db as DashboardDatabase;
}
// Update the last sync timestamp for a collection
public async updateLastSync(collection: string): Promise<void> {
await this.db.syncInfo.update(collection, { lastSync: Date.now() });
if (!isBrowser) return;
await (this.db as DashboardDatabase).syncInfo.update(collection, {
lastSync: Date.now(),
});
}
// Get the last sync timestamp for a collection
public async getLastSync(collection: string): Promise<number> {
const info = await this.db.syncInfo.get(collection);
if (!isBrowser) return 0;
const info = await (this.db as DashboardDatabase).syncInfo.get(collection);
return info?.lastSync || 0;
}
// Clear all data (useful for logout)
public async clearAllData(): Promise<void> {
await this.db.users.clear();
await this.db.events.clear();
await this.db.eventRequests.clear();
await this.db.logs.clear();
await this.db.officers.clear();
await this.db.reimbursements.clear();
await this.db.receipts.clear();
await this.db.sponsors.clear();
await this.db.offlineChanges.clear();
if (!isBrowser) return;
const db = this.db as DashboardDatabase;
await db.users.clear();
await db.events.clear();
await db.eventRequests.clear();
await db.logs.clear();
await db.officers.clear();
await db.reimbursements.clear();
await db.receipts.clear();
await db.sponsors.clear();
await db.offlineChanges.clear();
// Reset sync timestamps
const collections = [
'users', 'events', 'event_request', 'logs',
'officers', 'reimbursement', 'receipts', 'sponsors'
"users",
"events",
"event_request",
"logs",
"officers",
"reimbursement",
"receipts",
"sponsors",
];
for (const collection of collections) {
await this.db.syncInfo.update(collection, { lastSync: 0 });
await db.syncInfo.update(collection, { lastSync: 0 });
}
}
}

View file

@ -143,7 +143,7 @@ export class Get {
if (options?.expand) {
if (Array.isArray(options.expand)) {
expandString = options.expand.join(",");
} else if (typeof options.expand === 'string') {
} else if (typeof options.expand === "string") {
expandString = options.expand;
}
}
@ -196,7 +196,7 @@ export class Get {
if (options?.expand) {
if (Array.isArray(options.expand)) {
expandString = options.expand.join(",");
} else if (typeof options.expand === 'string') {
} else if (typeof options.expand === "string") {
expandString = options.expand;
}
}
@ -306,7 +306,7 @@ export class Get {
if (options?.expand) {
if (Array.isArray(options.expand)) {
expandString = options.expand.join(",");
} else if (typeof options.expand === 'string') {
} else if (typeof options.expand === "string") {
expandString = options.expand;
}
}