Refactors repository layer and IndexedDB architecture

Eliminates redundant repository abstraction layer by directly using EntityService methods

Implements key improvements:
- Removes unnecessary repository wrappers
- Introduces polymorphic DataSeeder for mock data loading
- Renames IndexedDBService to IndexedDBContext
- Fixes database injection timing with lazy access pattern
- Simplifies EventManager to use services directly

Reduces code complexity and improves separation of concerns
This commit is contained in:
Janus C. H. Knudsen 2025-11-20 21:45:09 +01:00
parent 5648c7c304
commit dcd76836bd
10 changed files with 1260 additions and 574 deletions

View file

@ -1,56 +0,0 @@
import { ICalendarEvent } from '../types/CalendarTypes';
/**
* Update source type
* - 'local': Changes made by the user locally (needs sync)
* - 'remote': Changes from API/SignalR (already synced)
*/
export type UpdateSource = 'local' | 'remote';
/**
* IEventRepository - Interface for event data access
*
* Abstracts the data source for calendar events, allowing easy switching
* between IndexedDB, REST API, GraphQL, or other data sources.
*
* Implementations:
* - IndexedDBEventRepository: Local storage with offline support
* - MockEventRepository: (Legacy) Loads from local JSON file
* - ApiEventRepository: (Future) Loads from backend API
*/
export interface IEventRepository {
/**
* Load all calendar events from the data source
* @returns Promise resolving to array of ICalendarEvent objects
* @throws Error if loading fails
*/
loadEvents(): Promise<ICalendarEvent[]>;
/**
* Create a new event
* @param event - Event to create (without ID, will be generated)
* @param source - Source of the update ('local' or 'remote')
* @returns Promise resolving to the created event with generated ID
* @throws Error if creation fails
*/
createEvent(event: Omit<ICalendarEvent, 'id'>, source?: UpdateSource): Promise<ICalendarEvent>;
/**
* Update an existing event
* @param id - ID of the event to update
* @param updates - Partial event data to update
* @param source - Source of the update ('local' or 'remote')
* @returns Promise resolving to the updated event
* @throws Error if update fails or event not found
*/
updateEvent(id: string, updates: Partial<ICalendarEvent>, source?: UpdateSource): Promise<ICalendarEvent>;
/**
* Delete an event
* @param id - ID of the event to delete
* @param source - Source of the update ('local' or 'remote')
* @returns Promise resolving when deletion is complete
* @throws Error if deletion fails or event not found
*/
deleteEvent(id: string, source?: UpdateSource): Promise<void>;
}

View file

@ -1,179 +0,0 @@
import { ICalendarEvent } from '../types/CalendarTypes';
import { IEventRepository, UpdateSource } from './IEventRepository';
import { IndexedDBService } from '../storage/IndexedDBService';
import { EventService } from '../storage/events/EventService';
import { OperationQueue } from '../storage/OperationQueue';
/**
* IndexedDBEventRepository
* Offline-first repository using IndexedDB as single source of truth
*
* All CRUD operations:
* - Save to IndexedDB immediately via EventService (always succeeds)
* - Add to sync queue if source is 'local'
* - Background SyncManager processes queue to sync with API
*/
export class IndexedDBEventRepository implements IEventRepository {
private indexedDB: IndexedDBService;
private eventService: EventService;
private queue: OperationQueue;
constructor(indexedDB: IndexedDBService, queue: OperationQueue) {
this.indexedDB = indexedDB;
this.queue = queue;
// EventService will be initialized after IndexedDB is ready
this.eventService = null as any;
}
/**
* Ensure EventService is initialized with database connection
*/
private ensureEventService(): void {
if (!this.eventService && this.indexedDB.isInitialized()) {
const db = (this.indexedDB as any).db; // Access private db property
this.eventService = new EventService(db);
}
}
/**
* Load all events from IndexedDB
* Ensures IndexedDB is initialized on first call
*/
async loadEvents(): Promise<ICalendarEvent[]> {
// Lazy initialization on first data load
if (!this.indexedDB.isInitialized()) {
await this.indexedDB.initialize();
// TODO: Seeding should be done at application level, not here
}
this.ensureEventService();
return await this.eventService.getAll();
}
/**
* Create a new event
* - Generates ID
* - Saves to IndexedDB
* - Adds to queue if local (needs sync)
*/
async createEvent(event: Omit<ICalendarEvent, 'id'>, source: UpdateSource = 'local'): Promise<ICalendarEvent> {
// Generate unique ID
const id = this.generateEventId();
// Determine sync status based on source
const syncStatus = source === 'local' ? 'pending' : 'synced';
// Create full event object
const newEvent: ICalendarEvent = {
...event,
id,
syncStatus
} as ICalendarEvent;
// Save to IndexedDB via EventService
this.ensureEventService();
await this.eventService.save(newEvent);
// If local change, add to sync queue
if (source === 'local') {
await this.queue.enqueue({
type: 'create',
entityId: id,
dataEntity: {
typename: 'Event',
data: newEvent
},
timestamp: Date.now(),
retryCount: 0
});
}
return newEvent;
}
/**
* Update an existing event
* - Updates in IndexedDB
* - Adds to queue if local (needs sync)
*/
async updateEvent(id: string, updates: Partial<ICalendarEvent>, source: UpdateSource = 'local'): Promise<ICalendarEvent> {
// Get existing event via EventService
this.ensureEventService();
const existingEvent = await this.eventService.get(id);
if (!existingEvent) {
throw new Error(`Event with ID ${id} not found`);
}
// Determine sync status based on source
const syncStatus = source === 'local' ? 'pending' : 'synced';
// Merge updates
const updatedEvent: ICalendarEvent = {
...existingEvent,
...updates,
id, // Ensure ID doesn't change
syncStatus
};
// Save to IndexedDB via EventService
await this.eventService.save(updatedEvent);
// If local change, add to sync queue
if (source === 'local') {
await this.queue.enqueue({
type: 'update',
entityId: id,
dataEntity: {
typename: 'Event',
data: updates
},
timestamp: Date.now(),
retryCount: 0
});
}
return updatedEvent;
}
/**
* Delete an event
* - Removes from IndexedDB
* - Adds to queue if local (needs sync)
*/
async deleteEvent(id: string, source: UpdateSource = 'local'): Promise<void> {
// Check if event exists via EventService
this.ensureEventService();
const existingEvent = await this.eventService.get(id);
if (!existingEvent) {
throw new Error(`Event with ID ${id} not found`);
}
// If local change, add to sync queue BEFORE deleting
// (so we can send the delete operation to API later)
if (source === 'local') {
await this.queue.enqueue({
type: 'delete',
entityId: id,
dataEntity: {
typename: 'Event',
data: { id } // Minimal data for delete - just ID
},
timestamp: Date.now(),
retryCount: 0
});
}
// Delete from IndexedDB via EventService
await this.eventService.delete(id);
}
/**
* Generate unique event ID
* Format: {timestamp}-{random}
*/
private generateEventId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
return `${timestamp}-${random}`;
}
}