Refactors calendar data management and sync infrastructure

Introduces comprehensive data management system for calendar V2
- Adds IndexedDB storage with pluggable entity services
- Implements EventBus for decoupled event communication
- Creates data seeding mechanism for initial application setup
- Establishes sync and repository abstractions for flexible data handling
This commit is contained in:
Janus C. H. Knudsen 2025-12-08 00:26:16 +01:00
parent dee977d4df
commit e581039b62
17 changed files with 1076 additions and 4 deletions

View file

@ -0,0 +1,32 @@
import { ICalendarEvent } from '../../types/CalendarTypes';
/**
* EventSerialization - Handles Date field serialization for IndexedDB
*
* IndexedDB doesn't store Date objects directly, so we convert:
* - Date ISO string (serialize) when writing
* - ISO string Date (deserialize) when reading
*/
export class EventSerialization {
/**
* Serialize event for IndexedDB storage
*/
static serialize(event: ICalendarEvent): unknown {
return {
...event,
start: event.start instanceof Date ? event.start.toISOString() : event.start,
end: event.end instanceof Date ? event.end.toISOString() : event.end
};
}
/**
* Deserialize event from IndexedDB storage
*/
static deserialize(data: Record<string, unknown>): ICalendarEvent {
return {
...data,
start: typeof data.start === 'string' ? new Date(data.start) : data.start,
end: typeof data.end === 'string' ? new Date(data.end) : data.end
} as ICalendarEvent;
}
}

View file

@ -0,0 +1,84 @@
import { ICalendarEvent, EntityType, IEventBus } from '../../types/CalendarTypes';
import { EventStore } from './EventStore';
import { EventSerialization } from './EventSerialization';
import { BaseEntityService } from '../BaseEntityService';
import { IndexedDBContext } from '../IndexedDBContext';
/**
* EventService - CRUD operations for calendar events in IndexedDB
*
* Extends BaseEntityService for shared CRUD and sync logic.
* Provides event-specific query methods.
*/
export class EventService extends BaseEntityService<ICalendarEvent> {
readonly storeName = EventStore.STORE_NAME;
readonly entityType: EntityType = 'Event';
constructor(context: IndexedDBContext, eventBus: IEventBus) {
super(context, eventBus);
}
protected serialize(event: ICalendarEvent): unknown {
return EventSerialization.serialize(event);
}
protected deserialize(data: unknown): ICalendarEvent {
return EventSerialization.deserialize(data as Record<string, unknown>);
}
/**
* Get events within a date range
*/
async getByDateRange(start: Date, end: Date): Promise<ICalendarEvent[]> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('start');
const range = IDBKeyRange.lowerBound(start.toISOString());
const request = index.getAll(range);
request.onsuccess = () => {
const data = request.result as unknown[];
const events = data
.map(item => this.deserialize(item))
.filter(event => event.start <= end);
resolve(events);
};
request.onerror = () => {
reject(new Error(`Failed to get events by date range: ${request.error}`));
};
});
}
/**
* Get events for a specific resource
*/
async getByResource(resourceId: string): Promise<ICalendarEvent[]> {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('resourceId');
const request = index.getAll(resourceId);
request.onsuccess = () => {
const data = request.result as unknown[];
const events = data.map(item => this.deserialize(item));
resolve(events);
};
request.onerror = () => {
reject(new Error(`Failed to get events for resource ${resourceId}: ${request.error}`));
};
});
}
/**
* Get events for a resource within a date range
*/
async getByResourceAndDateRange(resourceId: string, start: Date, end: Date): Promise<ICalendarEvent[]> {
const resourceEvents = await this.getByResource(resourceId);
return resourceEvents.filter(event => event.start >= start && event.start <= end);
}
}

View file

@ -0,0 +1,37 @@
import { IStore } from '../IStore';
/**
* EventStore - IndexedDB ObjectStore definition for calendar events
*/
export class EventStore implements IStore {
static readonly STORE_NAME = 'events';
readonly storeName = EventStore.STORE_NAME;
/**
* Create the events ObjectStore with indexes
*/
create(db: IDBDatabase): void {
const store = db.createObjectStore(EventStore.STORE_NAME, { keyPath: 'id' });
// Index: start (for date range queries)
store.createIndex('start', 'start', { unique: false });
// Index: end (for date range queries)
store.createIndex('end', 'end', { unique: false });
// Index: syncStatus (for filtering by sync state)
store.createIndex('syncStatus', 'syncStatus', { unique: false });
// Index: resourceId (for resource-mode filtering)
store.createIndex('resourceId', 'resourceId', { unique: false });
// Index: customerId (for customer-centric queries)
store.createIndex('customerId', 'customerId', { unique: false });
// Index: bookingId (for event-to-booking lookups)
store.createIndex('bookingId', 'bookingId', { unique: false });
// Compound index: startEnd (for optimized range queries)
store.createIndex('startEnd', ['start', 'end'], { unique: false });
}
}