Refactor storage architecture with modular IndexedDB services
Implements a more flexible, modular approach to IndexedDB storage: - Introduces IStore interface for dynamic store creation - Adds specialized services for each entity (BookingService, CustomerService, etc.) - Moves serialization logic to entity-specific classes - Improves dependency injection and extensibility Enables easier addition of new entity types with minimal configuration
This commit is contained in:
parent
88cccb3456
commit
2aa9d06fab
13 changed files with 1048 additions and 236 deletions
16
src/index.ts
16
src/index.ts
|
|
@ -29,6 +29,11 @@ import { IndexedDBEventRepository } from './repositories/IndexedDBEventRepositor
|
|||
import { ApiEventRepository } from './repositories/ApiEventRepository';
|
||||
import { IndexedDBService } from './storage/IndexedDBService';
|
||||
import { OperationQueue } from './storage/OperationQueue';
|
||||
import { IStore } from './storage/IStore';
|
||||
import { BookingStore } from './storage/bookings/BookingStore';
|
||||
import { CustomerStore } from './storage/customers/CustomerStore';
|
||||
import { ResourceStore } from './storage/resources/ResourceStore';
|
||||
import { EventStore } from './storage/events/EventStore';
|
||||
|
||||
// Import workers
|
||||
import { SyncManager } from './workers/SyncManager';
|
||||
|
|
@ -94,6 +99,17 @@ async function initializeCalendar(): Promise<void> {
|
|||
// Register configuration instance
|
||||
builder.registerInstance(config).as<Configuration>();
|
||||
|
||||
// Register storage stores (IStore implementations)
|
||||
// Open/Closed Principle: Adding new entity only requires adding one line here
|
||||
builder.registerType(BookingStore).as<IStore>();
|
||||
builder.registerType(CustomerStore).as<IStore>();
|
||||
builder.registerType(ResourceStore).as<IStore>();
|
||||
builder.registerType(EventStore).as<IStore>();
|
||||
|
||||
// Resolve all IStore implementations and register as array
|
||||
const stores = container.resolveTypeAll<IStore>();
|
||||
builder.registerInstance(stores).as<IStore[]>();
|
||||
|
||||
// Register storage and repository services
|
||||
builder.registerType(IndexedDBService).as<IndexedDBService>();
|
||||
builder.registerType(OperationQueue).as<OperationQueue>();
|
||||
|
|
|
|||
25
src/storage/IStore.ts
Normal file
25
src/storage/IStore.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* IStore - Interface for IndexedDB ObjectStore definitions
|
||||
*
|
||||
* Each entity store (bookings, customers, resources, events) implements this interface
|
||||
* to define its schema and creation logic.
|
||||
*
|
||||
* This enables Open/Closed Principle: IndexedDBService can work with any IStore
|
||||
* implementation without modification. Adding new entities only requires:
|
||||
* 1. Create new Store class implementing IStore
|
||||
* 2. Register in DI container as IStore
|
||||
*/
|
||||
export interface IStore {
|
||||
/**
|
||||
* The name of the ObjectStore in IndexedDB
|
||||
*/
|
||||
readonly storeName: string;
|
||||
|
||||
/**
|
||||
* Create the ObjectStore with its schema (indexes, keyPath, etc.)
|
||||
* Called during database upgrade (onupgradeneeded event)
|
||||
*
|
||||
* @param db - IDBDatabase instance
|
||||
*/
|
||||
create(db: IDBDatabase): void;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { IBooking } from '../types/BookingTypes';
|
||||
import { IStore } from './IStore';
|
||||
|
||||
/**
|
||||
* Operation for the sync queue
|
||||
|
|
@ -15,20 +15,30 @@ export interface IQueueOperation {
|
|||
|
||||
/**
|
||||
* IndexedDB Service for Calendar App
|
||||
* Handles local storage of events and sync queue
|
||||
* Handles database connection management and core operations
|
||||
*
|
||||
* Entity-specific CRUD operations are handled by specialized services:
|
||||
* - EventService for calendar events
|
||||
* - BookingService for bookings
|
||||
* - CustomerService for customers
|
||||
* - ResourceService for resources
|
||||
*/
|
||||
export class IndexedDBService {
|
||||
private static readonly DB_NAME = 'CalendarDB';
|
||||
private static readonly DB_VERSION = 2;
|
||||
private static readonly EVENTS_STORE = 'events';
|
||||
private static readonly QUEUE_STORE = 'operationQueue';
|
||||
private static readonly SYNC_STATE_STORE = 'syncState';
|
||||
private static readonly BOOKINGS_STORE = 'bookings';
|
||||
private static readonly CUSTOMERS_STORE = 'customers';
|
||||
private static readonly RESOURCES_STORE = 'resources';
|
||||
|
||||
private db: IDBDatabase | null = null;
|
||||
private initialized: boolean = false;
|
||||
private stores: IStore[];
|
||||
|
||||
/**
|
||||
* @param stores - Array of IStore implementations injected via DI
|
||||
*/
|
||||
constructor(stores: IStore[]) {
|
||||
this.stores = stores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and open the database
|
||||
|
|
@ -49,69 +59,25 @@ export class IndexedDBService {
|
|||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
const oldVersion = (event as IDBVersionChangeEvent).oldVersion;
|
||||
|
||||
// Create events store
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.EVENTS_STORE)) {
|
||||
const eventsStore = db.createObjectStore(IndexedDBService.EVENTS_STORE, { keyPath: 'id' });
|
||||
eventsStore.createIndex('start', 'start', { unique: false });
|
||||
eventsStore.createIndex('end', 'end', { unique: false });
|
||||
eventsStore.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
eventsStore.createIndex('resourceId', 'resourceId', { unique: false });
|
||||
eventsStore.createIndex('customerId', 'customerId', { unique: false });
|
||||
eventsStore.createIndex('bookingId', 'bookingId', { unique: false });
|
||||
eventsStore.createIndex('startEnd', ['start', 'end'], { unique: false });
|
||||
} else if (oldVersion < 2) {
|
||||
// Upgrade from version 1: Add new indexes to existing events store
|
||||
const transaction = (event.target as IDBOpenDBRequest).transaction!;
|
||||
const eventsStore = transaction.objectStore(IndexedDBService.EVENTS_STORE);
|
||||
// Create all entity stores via injected IStore implementations
|
||||
// Open/Closed Principle: Adding new entity only requires DI registration
|
||||
this.stores.forEach(store => {
|
||||
if (!db.objectStoreNames.contains(store.storeName)) {
|
||||
store.create(db);
|
||||
}
|
||||
});
|
||||
|
||||
if (!eventsStore.indexNames.contains('resourceId')) {
|
||||
eventsStore.createIndex('resourceId', 'resourceId', { unique: false });
|
||||
}
|
||||
if (!eventsStore.indexNames.contains('customerId')) {
|
||||
eventsStore.createIndex('customerId', 'customerId', { unique: false });
|
||||
}
|
||||
if (!eventsStore.indexNames.contains('bookingId')) {
|
||||
eventsStore.createIndex('bookingId', 'bookingId', { unique: false });
|
||||
}
|
||||
if (!eventsStore.indexNames.contains('startEnd')) {
|
||||
eventsStore.createIndex('startEnd', ['start', 'end'], { unique: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Create operation queue store
|
||||
// Create operation queue store (sync infrastructure)
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.QUEUE_STORE)) {
|
||||
const queueStore = db.createObjectStore(IndexedDBService.QUEUE_STORE, { keyPath: 'id' });
|
||||
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
}
|
||||
|
||||
// Create sync state store
|
||||
// Create sync state store (sync metadata)
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.SYNC_STATE_STORE)) {
|
||||
db.createObjectStore(IndexedDBService.SYNC_STATE_STORE, { keyPath: 'key' });
|
||||
}
|
||||
|
||||
// Create bookings store (v2)
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.BOOKINGS_STORE)) {
|
||||
const bookingsStore = db.createObjectStore(IndexedDBService.BOOKINGS_STORE, { keyPath: 'id' });
|
||||
bookingsStore.createIndex('customerId', 'customerId', { unique: false });
|
||||
bookingsStore.createIndex('status', 'status', { unique: false });
|
||||
bookingsStore.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
}
|
||||
|
||||
// Create customers store (v2)
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.CUSTOMERS_STORE)) {
|
||||
const customersStore = db.createObjectStore(IndexedDBService.CUSTOMERS_STORE, { keyPath: 'id' });
|
||||
customersStore.createIndex('name', 'name', { unique: false });
|
||||
customersStore.createIndex('phone', 'phone', { unique: false });
|
||||
}
|
||||
|
||||
// Create resources store (v2)
|
||||
if (!db.objectStoreNames.contains(IndexedDBService.RESOURCES_STORE)) {
|
||||
const resourcesStore = db.createObjectStore(IndexedDBService.RESOURCES_STORE, { keyPath: 'id' });
|
||||
resourcesStore.createIndex('type', 'type', { unique: false });
|
||||
resourcesStore.createIndex('isActive', 'isActive', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -134,92 +100,10 @@ export class IndexedDBService {
|
|||
}
|
||||
|
||||
// ========================================
|
||||
// Event CRUD Operations
|
||||
// Event CRUD Operations - MOVED TO EventService
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get a single event by ID
|
||||
*/
|
||||
async getEvent(id: string): Promise<ICalendarEvent | null> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const event = request.result as ICalendarEvent | undefined;
|
||||
resolve(event ? this.deserializeEvent(event) : null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get event ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events
|
||||
*/
|
||||
async getAllEvents(): Promise<ICalendarEvent[]> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const events = request.result as ICalendarEvent[];
|
||||
resolve(events.map(e => this.deserializeEvent(e)));
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all events: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an event (create or update)
|
||||
*/
|
||||
async saveEvent(event: ICalendarEvent): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
const serialized = this.serializeEvent(event);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save event ${event.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event
|
||||
*/
|
||||
async deleteEvent(id: string): Promise<void> {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBService.EVENTS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBService.EVENTS_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete event ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
// Event operations have been moved to storage/events/EventService.ts
|
||||
// for better modularity and separation of concerns.
|
||||
|
||||
// ========================================
|
||||
// Queue Operations
|
||||
|
|
@ -356,52 +240,6 @@ export class IndexedDBService {
|
|||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Serialization Helpers
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Serialize event for IndexedDB storage (convert Dates to ISO strings)
|
||||
*/
|
||||
private serializeEvent(event: ICalendarEvent): any {
|
||||
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 (convert ISO strings to Dates)
|
||||
*/
|
||||
private deserializeEvent(event: any): ICalendarEvent {
|
||||
return {
|
||||
...event,
|
||||
start: typeof event.start === 'string' ? new Date(event.start) : event.start,
|
||||
end: typeof event.end === 'string' ? new Date(event.end) : event.end
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize booking for IndexedDB storage (convert Dates to ISO strings)
|
||||
*/
|
||||
private serializeBooking(booking: IBooking): any {
|
||||
return {
|
||||
...booking,
|
||||
createdAt: booking.createdAt instanceof Date ? booking.createdAt.toISOString() : booking.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize booking from IndexedDB (convert ISO strings to Dates)
|
||||
*/
|
||||
private deserializeBooking(booking: any): IBooking {
|
||||
return {
|
||||
...booking,
|
||||
createdAt: typeof booking.createdAt === 'string' ? new Date(booking.createdAt) : booking.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
|
|
@ -429,50 +267,10 @@ export class IndexedDBService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed IndexedDB with mock data if empty
|
||||
*/
|
||||
async seedIfEmpty(mockDataUrl: string = 'data/mock-events.json'): Promise<void> {
|
||||
try {
|
||||
const existingEvents = await this.getAllEvents();
|
||||
|
||||
if (existingEvents.length > 0) {
|
||||
console.log(`IndexedDB already has ${existingEvents.length} events - skipping seed`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('IndexedDB is empty - seeding with mock data');
|
||||
|
||||
// Check if online to fetch mock data
|
||||
if (!navigator.onLine) {
|
||||
console.warn('Offline and IndexedDB empty - starting with no events');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch mock events
|
||||
const response = await fetch(mockDataUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch mock events: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const mockEvents = await response.json();
|
||||
|
||||
// Convert and save to IndexedDB
|
||||
for (const event of mockEvents) {
|
||||
const calendarEvent = {
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end),
|
||||
allDay: event.allDay || false,
|
||||
syncStatus: 'synced' as const
|
||||
};
|
||||
await this.saveEvent(calendarEvent);
|
||||
}
|
||||
|
||||
console.log(`Seeded IndexedDB with ${mockEvents.length} mock events`);
|
||||
} catch (error) {
|
||||
console.error('Failed to seed IndexedDB:', error);
|
||||
// Don't throw - allow app to start with empty calendar
|
||||
}
|
||||
}
|
||||
// ========================================
|
||||
// Seeding - REMOVED
|
||||
// ========================================
|
||||
// seedIfEmpty() has been removed.
|
||||
// Seeding should be implemented at application level using EventService,
|
||||
// BookingService, CustomerService, and ResourceService directly.
|
||||
}
|
||||
|
|
|
|||
42
src/storage/bookings/BookingSerialization.ts
Normal file
42
src/storage/bookings/BookingSerialization.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { IBooking } from '../../types/BookingTypes';
|
||||
|
||||
/**
|
||||
* BookingSerialization - Handles Date field serialization for IndexedDB
|
||||
*
|
||||
* IndexedDB doesn't store Date objects directly, so we convert:
|
||||
* - Date → ISO string (serialize) when writing to IndexedDB
|
||||
* - ISO string → Date (deserialize) when reading from IndexedDB
|
||||
*/
|
||||
export class BookingSerialization {
|
||||
/**
|
||||
* Serialize booking for IndexedDB storage
|
||||
* Converts Date fields to ISO strings
|
||||
*
|
||||
* @param booking - IBooking with Date objects
|
||||
* @returns Plain object with ISO string dates
|
||||
*/
|
||||
static serialize(booking: IBooking): any {
|
||||
return {
|
||||
...booking,
|
||||
createdAt: booking.createdAt instanceof Date
|
||||
? booking.createdAt.toISOString()
|
||||
: booking.createdAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize booking from IndexedDB storage
|
||||
* Converts ISO string dates back to Date objects
|
||||
*
|
||||
* @param data - Plain object from IndexedDB with ISO string dates
|
||||
* @returns IBooking with Date objects
|
||||
*/
|
||||
static deserialize(data: any): IBooking {
|
||||
return {
|
||||
...data,
|
||||
createdAt: typeof data.createdAt === 'string'
|
||||
? new Date(data.createdAt)
|
||||
: data.createdAt
|
||||
};
|
||||
}
|
||||
}
|
||||
164
src/storage/bookings/BookingService.ts
Normal file
164
src/storage/bookings/BookingService.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { IBooking } from '../../types/BookingTypes';
|
||||
import { BookingStore } from './BookingStore';
|
||||
import { BookingSerialization } from './BookingSerialization';
|
||||
|
||||
/**
|
||||
* BookingService - CRUD operations for bookings in IndexedDB
|
||||
*
|
||||
* Handles all booking-related database operations.
|
||||
* Part of modular storage architecture where each entity has its own service.
|
||||
*/
|
||||
export class BookingService {
|
||||
private db: IDBDatabase;
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single booking by ID
|
||||
*
|
||||
* @param id - Booking ID
|
||||
* @returns IBooking or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<IBooking | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result;
|
||||
if (data) {
|
||||
resolve(BookingSerialization.deserialize(data));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get booking ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bookings
|
||||
*
|
||||
* @returns Array of all bookings
|
||||
*/
|
||||
async getAll(): Promise<IBooking[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const bookings = data.map(item => BookingSerialization.deserialize(item));
|
||||
resolve(bookings);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all bookings: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a booking (create or update)
|
||||
*
|
||||
* @param booking - IBooking to save
|
||||
*/
|
||||
async save(booking: IBooking): Promise<void> {
|
||||
const serialized = BookingSerialization.serialize(booking);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save booking ${booking.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a booking
|
||||
*
|
||||
* @param id - Booking ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete booking ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookings by customer ID
|
||||
*
|
||||
* @param customerId - Customer ID
|
||||
* @returns Array of bookings for this customer
|
||||
*/
|
||||
async getByCustomer(customerId: string): Promise<IBooking[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const index = store.index('customerId');
|
||||
const request = index.getAll(customerId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const bookings = data.map(item => BookingSerialization.deserialize(item));
|
||||
resolve(bookings);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get bookings for customer ${customerId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookings by status
|
||||
*
|
||||
* @param status - Booking status
|
||||
* @returns Array of bookings with this status
|
||||
*/
|
||||
async getByStatus(status: string): Promise<IBooking[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([BookingStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(BookingStore.STORE_NAME);
|
||||
const index = store.index('status');
|
||||
const request = index.getAll(status);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const bookings = data.map(item => BookingSerialization.deserialize(item));
|
||||
resolve(bookings);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get bookings with status ${status}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
35
src/storage/bookings/BookingStore.ts
Normal file
35
src/storage/bookings/BookingStore.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
/**
|
||||
* BookingStore - IndexedDB ObjectStore definition for bookings
|
||||
*
|
||||
* Defines schema, indexes, and store creation logic.
|
||||
* Part of modular storage architecture where each entity has its own folder.
|
||||
*/
|
||||
export class BookingStore implements IStore {
|
||||
/** ObjectStore name in IndexedDB (static for backward compatibility) */
|
||||
static readonly STORE_NAME = 'bookings';
|
||||
|
||||
/** ObjectStore name in IndexedDB (instance property for IStore interface) */
|
||||
readonly storeName = BookingStore.STORE_NAME;
|
||||
|
||||
/**
|
||||
* Create the bookings ObjectStore with indexes
|
||||
* Called during database upgrade (onupgradeneeded)
|
||||
*
|
||||
* @param db - IDBDatabase instance
|
||||
*/
|
||||
create(db: IDBDatabase): void {
|
||||
// Create ObjectStore with 'id' as keyPath
|
||||
const store = db.createObjectStore(BookingStore.STORE_NAME, { keyPath: 'id' });
|
||||
|
||||
// Index: customerId (for querying bookings by customer)
|
||||
store.createIndex('customerId', 'customerId', { unique: false });
|
||||
|
||||
// Index: status (for filtering by booking status)
|
||||
store.createIndex('status', 'status', { unique: false });
|
||||
|
||||
// Index: createdAt (for sorting bookings chronologically)
|
||||
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
}
|
||||
}
|
||||
144
src/storage/customers/CustomerService.ts
Normal file
144
src/storage/customers/CustomerService.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { ICustomer } from '../../types/CustomerTypes';
|
||||
import { CustomerStore } from './CustomerStore';
|
||||
|
||||
/**
|
||||
* CustomerService - CRUD operations for customers in IndexedDB
|
||||
*
|
||||
* Handles all customer-related database operations.
|
||||
* Part of modular storage architecture where each entity has its own service.
|
||||
*
|
||||
* Note: No serialization needed - ICustomer has no Date fields.
|
||||
*/
|
||||
export class CustomerService {
|
||||
private db: IDBDatabase;
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single customer by ID
|
||||
*
|
||||
* @param id - Customer ID
|
||||
* @returns ICustomer or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<ICustomer | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(CustomerStore.STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get customer ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all customers
|
||||
*
|
||||
* @returns Array of all customers
|
||||
*/
|
||||
async getAll(): Promise<ICustomer[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(CustomerStore.STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as ICustomer[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all customers: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a customer (create or update)
|
||||
*
|
||||
* @param customer - ICustomer to save
|
||||
*/
|
||||
async save(customer: ICustomer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(CustomerStore.STORE_NAME);
|
||||
const request = store.put(customer);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save customer ${customer.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a customer
|
||||
*
|
||||
* @param id - Customer ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(CustomerStore.STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete customer ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customers by phone number
|
||||
*
|
||||
* @param phone - Phone number
|
||||
* @returns Array of customers with this phone
|
||||
*/
|
||||
async getByPhone(phone: string): Promise<ICustomer[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([CustomerStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(CustomerStore.STORE_NAME);
|
||||
const index = store.index('phone');
|
||||
const request = index.getAll(phone);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as ICustomer[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get customers by phone ${phone}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search customers by name (partial match)
|
||||
*
|
||||
* @param searchTerm - Search term (case insensitive)
|
||||
* @returns Array of customers matching search
|
||||
*/
|
||||
async searchByName(searchTerm: string): Promise<ICustomer[]> {
|
||||
const allCustomers = await this.getAll();
|
||||
const lowerSearch = searchTerm.toLowerCase();
|
||||
|
||||
return allCustomers.filter(customer =>
|
||||
customer.name.toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
}
|
||||
}
|
||||
32
src/storage/customers/CustomerStore.ts
Normal file
32
src/storage/customers/CustomerStore.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
/**
|
||||
* CustomerStore - IndexedDB ObjectStore definition for customers
|
||||
*
|
||||
* Defines schema, indexes, and store creation logic.
|
||||
* Part of modular storage architecture where each entity has its own folder.
|
||||
*/
|
||||
export class CustomerStore implements IStore {
|
||||
/** ObjectStore name in IndexedDB (static for backward compatibility) */
|
||||
static readonly STORE_NAME = 'customers';
|
||||
|
||||
/** ObjectStore name in IndexedDB (instance property for IStore interface) */
|
||||
readonly storeName = CustomerStore.STORE_NAME;
|
||||
|
||||
/**
|
||||
* Create the customers ObjectStore with indexes
|
||||
* Called during database upgrade (onupgradeneeded)
|
||||
*
|
||||
* @param db - IDBDatabase instance
|
||||
*/
|
||||
create(db: IDBDatabase): void {
|
||||
// Create ObjectStore with 'id' as keyPath
|
||||
const store = db.createObjectStore(CustomerStore.STORE_NAME, { keyPath: 'id' });
|
||||
|
||||
// Index: name (for customer search/lookup)
|
||||
store.createIndex('name', 'name', { unique: false });
|
||||
|
||||
// Index: phone (for customer lookup by phone)
|
||||
store.createIndex('phone', 'phone', { unique: false });
|
||||
}
|
||||
}
|
||||
40
src/storage/events/EventSerialization.ts
Normal file
40
src/storage/events/EventSerialization.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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 to IndexedDB
|
||||
* - ISO string → Date (deserialize) when reading from IndexedDB
|
||||
*/
|
||||
export class EventSerialization {
|
||||
/**
|
||||
* Serialize event for IndexedDB storage
|
||||
* Converts Date fields to ISO strings
|
||||
*
|
||||
* @param event - ICalendarEvent with Date objects
|
||||
* @returns Plain object with ISO string dates
|
||||
*/
|
||||
static serialize(event: ICalendarEvent): any {
|
||||
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
|
||||
* Converts ISO string dates back to Date objects
|
||||
*
|
||||
* @param data - Plain object from IndexedDB with ISO string dates
|
||||
* @returns ICalendarEvent with Date objects
|
||||
*/
|
||||
static deserialize(data: any): 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
|
||||
};
|
||||
}
|
||||
}
|
||||
264
src/storage/events/EventService.ts
Normal file
264
src/storage/events/EventService.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
import { ICalendarEvent } from '../../types/CalendarTypes';
|
||||
import { EventStore } from './EventStore';
|
||||
import { EventSerialization } from './EventSerialization';
|
||||
|
||||
/**
|
||||
* EventService - CRUD operations for calendar events in IndexedDB
|
||||
*
|
||||
* Handles all event-related database operations.
|
||||
* Part of modular storage architecture where each entity has its own service.
|
||||
*/
|
||||
export class EventService {
|
||||
private db: IDBDatabase;
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by ID
|
||||
*
|
||||
* @param id - Event ID
|
||||
* @returns ICalendarEvent or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<ICalendarEvent | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result;
|
||||
if (data) {
|
||||
resolve(EventSerialization.deserialize(data));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get event ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events
|
||||
*
|
||||
* @returns Array of all events
|
||||
*/
|
||||
async getAll(): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all events: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an event (create or update)
|
||||
*
|
||||
* @param event - ICalendarEvent to save
|
||||
*/
|
||||
async save(event: ICalendarEvent): Promise<void> {
|
||||
const serialized = EventSerialization.serialize(event);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save event ${event.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event
|
||||
*
|
||||
* @param id - Event ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete event ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events within a date range
|
||||
* Uses start index + in-memory filtering for simplicity and performance
|
||||
*
|
||||
* @param start - Start date (inclusive)
|
||||
* @param end - End date (inclusive)
|
||||
* @returns Array of events in range
|
||||
*/
|
||||
async getByDateRange(start: Date, end: Date): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const index = store.index('start');
|
||||
|
||||
// Get all events starting from start date
|
||||
const range = IDBKeyRange.lowerBound(start.toISOString());
|
||||
const request = index.getAll(range);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
|
||||
// Deserialize and filter in memory
|
||||
const events = data
|
||||
.map(item => EventSerialization.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
|
||||
*
|
||||
* @param resourceId - Resource ID
|
||||
* @returns Array of events for this resource
|
||||
*/
|
||||
async getByResource(resourceId: string): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const index = store.index('resourceId');
|
||||
const request = index.getAll(resourceId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get events for resource ${resourceId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events for a specific customer
|
||||
*
|
||||
* @param customerId - Customer ID
|
||||
* @returns Array of events for this customer
|
||||
*/
|
||||
async getByCustomer(customerId: string): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const index = store.index('customerId');
|
||||
const request = index.getAll(customerId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get events for customer ${customerId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events for a specific booking
|
||||
*
|
||||
* @param bookingId - Booking ID
|
||||
* @returns Array of events for this booking
|
||||
*/
|
||||
async getByBooking(bookingId: string): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const index = store.index('bookingId');
|
||||
const request = index.getAll(bookingId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get events for booking ${bookingId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by sync status
|
||||
*
|
||||
* @param syncStatus - Sync status ('synced', 'pending', 'error')
|
||||
* @returns Array of events with this sync status
|
||||
*/
|
||||
async getBySyncStatus(syncStatus: string): Promise<ICalendarEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([EventStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(EventStore.STORE_NAME);
|
||||
const index = store.index('syncStatus');
|
||||
const request = index.getAll(syncStatus);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get events by sync status ${syncStatus}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events for a resource within a date range
|
||||
* Combines resource and date filtering
|
||||
*
|
||||
* @param resourceId - Resource ID
|
||||
* @param start - Start date
|
||||
* @param end - End date
|
||||
* @returns Array of events for this resource in range
|
||||
*/
|
||||
async getByResourceAndDateRange(resourceId: string, start: Date, end: Date): Promise<ICalendarEvent[]> {
|
||||
// Get events for resource, then filter by date in memory
|
||||
const resourceEvents = await this.getByResource(resourceId);
|
||||
return resourceEvents.filter(event => event.start >= start && event.start <= end);
|
||||
}
|
||||
}
|
||||
47
src/storage/events/EventStore.ts
Normal file
47
src/storage/events/EventStore.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
/**
|
||||
* EventStore - IndexedDB ObjectStore definition for calendar events
|
||||
*
|
||||
* Defines schema, indexes, and store creation logic.
|
||||
* Part of modular storage architecture where each entity has its own folder.
|
||||
*/
|
||||
export class EventStore implements IStore {
|
||||
/** ObjectStore name in IndexedDB (static for backward compatibility) */
|
||||
static readonly STORE_NAME = 'events';
|
||||
|
||||
/** ObjectStore name in IndexedDB (instance property for IStore interface) */
|
||||
readonly storeName = EventStore.STORE_NAME;
|
||||
|
||||
/**
|
||||
* Create the events ObjectStore with indexes
|
||||
* Called during database upgrade (onupgradeneeded)
|
||||
*
|
||||
* @param db - IDBDatabase instance
|
||||
*/
|
||||
create(db: IDBDatabase): void {
|
||||
// Create ObjectStore with 'id' as keyPath
|
||||
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 (CRITICAL 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 });
|
||||
}
|
||||
}
|
||||
173
src/storage/resources/ResourceService.ts
Normal file
173
src/storage/resources/ResourceService.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { IResource } from '../../types/ResourceTypes';
|
||||
import { ResourceStore } from './ResourceStore';
|
||||
|
||||
/**
|
||||
* ResourceService - CRUD operations for resources in IndexedDB
|
||||
*
|
||||
* Handles all resource-related database operations.
|
||||
* Part of modular storage architecture where each entity has its own service.
|
||||
*
|
||||
* Note: No serialization needed - IResource has no Date fields.
|
||||
*/
|
||||
export class ResourceService {
|
||||
private db: IDBDatabase;
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single resource by ID
|
||||
*
|
||||
* @param id - Resource ID
|
||||
* @returns IResource or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<IResource | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get resource ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all resources
|
||||
*
|
||||
* @returns Array of all resources
|
||||
*/
|
||||
async getAll(): Promise<IResource[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all resources: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a resource (create or update)
|
||||
*
|
||||
* @param resource - IResource to save
|
||||
*/
|
||||
async save(resource: IResource): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const request = store.put(resource);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save resource ${resource.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a resource
|
||||
*
|
||||
* @param id - Resource ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete resource ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resources by type
|
||||
*
|
||||
* @param type - Resource type (person, room, equipment, etc.)
|
||||
* @returns Array of resources of this type
|
||||
*/
|
||||
async getByType(type: string): Promise<IResource[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const index = store.index('type');
|
||||
const request = index.getAll(type);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get resources by type ${type}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active resources only
|
||||
*
|
||||
* @returns Array of active resources (isActive = true)
|
||||
*/
|
||||
async getActive(): Promise<IResource[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const index = store.index('isActive');
|
||||
const request = index.getAll(true);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get active resources: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inactive resources
|
||||
*
|
||||
* @returns Array of inactive resources (isActive = false)
|
||||
*/
|
||||
async getInactive(): Promise<IResource[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([ResourceStore.STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(ResourceStore.STORE_NAME);
|
||||
const index = store.index('isActive');
|
||||
const request = index.getAll(false);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get inactive resources: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
32
src/storage/resources/ResourceStore.ts
Normal file
32
src/storage/resources/ResourceStore.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
/**
|
||||
* ResourceStore - IndexedDB ObjectStore definition for resources
|
||||
*
|
||||
* Defines schema, indexes, and store creation logic.
|
||||
* Part of modular storage architecture where each entity has its own folder.
|
||||
*/
|
||||
export class ResourceStore implements IStore {
|
||||
/** ObjectStore name in IndexedDB (static for backward compatibility) */
|
||||
static readonly STORE_NAME = 'resources';
|
||||
|
||||
/** ObjectStore name in IndexedDB (instance property for IStore interface) */
|
||||
readonly storeName = ResourceStore.STORE_NAME;
|
||||
|
||||
/**
|
||||
* Create the resources ObjectStore with indexes
|
||||
* Called during database upgrade (onupgradeneeded)
|
||||
*
|
||||
* @param db - IDBDatabase instance
|
||||
*/
|
||||
create(db: IDBDatabase): void {
|
||||
// Create ObjectStore with 'id' as keyPath
|
||||
const store = db.createObjectStore(ResourceStore.STORE_NAME, { keyPath: 'id' });
|
||||
|
||||
// Index: type (for filtering by resource category)
|
||||
store.createIndex('type', 'type', { unique: false });
|
||||
|
||||
// Index: isActive (for showing/hiding inactive resources)
|
||||
store.createIndex('isActive', 'isActive', { unique: false });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue