Refactor entity services with hybrid sync pattern
Introduces BaseEntityService and SyncPlugin to eliminate code duplication across entity services Improves: - Code reusability through inheritance and composition - Sync infrastructure for all entity types - Polymorphic sync status management - Reduced boilerplate code by ~75% Supports generic sync for Event, Booking, Customer, and Resource entities
This commit is contained in:
parent
2aa9d06fab
commit
8e52d670d6
30 changed files with 1960 additions and 526 deletions
211
src/storage/BaseEntityService.ts
Normal file
211
src/storage/BaseEntityService.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes';
|
||||
import { IEntityService } from './IEntityService';
|
||||
import { SyncPlugin } from './SyncPlugin';
|
||||
|
||||
/**
|
||||
* BaseEntityService<T extends ISync> - Abstract base class for all entity services
|
||||
*
|
||||
* HYBRID PATTERN: Inheritance + Composition
|
||||
* - Services EXTEND this base class (inheritance for structure)
|
||||
* - Sync logic is COMPOSED via SyncPlugin (pluggable)
|
||||
*
|
||||
* PROVIDES:
|
||||
* - Generic CRUD operations (get, getAll, save, delete)
|
||||
* - Sync status management (delegates to SyncPlugin)
|
||||
* - Serialization hooks (override in subclass if needed)
|
||||
*
|
||||
* SUBCLASSES MUST IMPLEMENT:
|
||||
* - storeName: string (IndexedDB object store name)
|
||||
* - entityType: EntityType (for runtime routing)
|
||||
*
|
||||
* SUBCLASSES MAY OVERRIDE:
|
||||
* - serialize(entity: T): any (default: no serialization)
|
||||
* - deserialize(data: any): T (default: no deserialization)
|
||||
*
|
||||
* BENEFITS:
|
||||
* - DRY: Single source of truth for CRUD logic
|
||||
* - Type safety: Generic T ensures compile-time checking
|
||||
* - Pluggable: SyncPlugin can be swapped for testing/different implementations
|
||||
* - Open/Closed: New entities just extend this class
|
||||
*/
|
||||
export abstract class BaseEntityService<T extends ISync> implements IEntityService<T> {
|
||||
// Abstract properties - must be implemented by subclasses
|
||||
abstract readonly storeName: string;
|
||||
abstract readonly entityType: EntityType;
|
||||
|
||||
// Internal composition - sync functionality
|
||||
private syncPlugin: SyncPlugin<T>;
|
||||
|
||||
// Protected database instance - accessible to subclasses
|
||||
protected db: IDBDatabase;
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
this.syncPlugin = new SyncPlugin<T>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize entity before storing in IndexedDB
|
||||
* Override in subclass if entity has Date fields or needs transformation
|
||||
*
|
||||
* @param entity - Entity to serialize
|
||||
* @returns Serialized data (default: entity itself)
|
||||
*/
|
||||
protected serialize(entity: T): any {
|
||||
return entity; // Default: no serialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data from IndexedDB back to entity
|
||||
* Override in subclass if entity has Date fields or needs transformation
|
||||
*
|
||||
* @param data - Raw data from IndexedDB
|
||||
* @returns Deserialized entity (default: data itself)
|
||||
*/
|
||||
protected deserialize(data: any): T {
|
||||
return data as T; // Default: no deserialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single entity by ID
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns Entity or null if not found
|
||||
*/
|
||||
async get(id: string): Promise<T | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result;
|
||||
if (data) {
|
||||
resolve(this.deserialize(data));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities
|
||||
*
|
||||
* @returns Array of all entities
|
||||
*/
|
||||
async getAll(): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const entities = data.map(item => this.deserialize(item));
|
||||
resolve(entities);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get all ${this.entityType}s: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an entity (create or update)
|
||||
*
|
||||
* @param entity - Entity to save
|
||||
*/
|
||||
async save(entity: T): Promise<void> {
|
||||
const serialized = this.serialize(entity);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save ${this.entityType} ${(entity as any).id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entity
|
||||
*
|
||||
* @param id - Entity ID to delete
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to delete ${this.entityType} ${id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYNC METHODS (IEntityService implementation) - Delegates to SyncPlugin
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Mark entity as successfully synced (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsSynced(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsSynced(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark entity as sync error (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsError(id: string): Promise<void> {
|
||||
return this.syncPlugin.markAsError(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status for an entity (IEntityService implementation)
|
||||
* Delegates to SyncPlugin
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns SyncStatus or null if entity not found
|
||||
*/
|
||||
async getSyncStatus(id: string): Promise<SyncStatus | null> {
|
||||
return this.syncPlugin.getSyncStatus(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entities by sync status
|
||||
* Delegates to SyncPlugin - uses IndexedDB syncStatus index
|
||||
*
|
||||
* @param syncStatus - Sync status ('synced', 'pending', 'error')
|
||||
* @returns Array of entities with this sync status
|
||||
*/
|
||||
async getBySyncStatus(syncStatus: string): Promise<T[]> {
|
||||
return this.syncPlugin.getBySyncStatus(syncStatus);
|
||||
}
|
||||
}
|
||||
46
src/storage/IEntityService.ts
Normal file
46
src/storage/IEntityService.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* IEntityService<T> - Generic interface for entity services with sync capabilities
|
||||
*
|
||||
* All entity services (Event, Booking, Customer, Resource) implement this interface
|
||||
* to enable polymorphic sync status management in SyncManager.
|
||||
*
|
||||
* ENCAPSULATION: Services encapsulate sync status manipulation.
|
||||
* SyncManager does NOT directly manipulate entity.syncStatus - it delegates to the service.
|
||||
*
|
||||
* POLYMORFI: SyncManager works with Array<IEntityService<any>> and uses
|
||||
* entityType property for runtime routing, avoiding switch statements.
|
||||
*/
|
||||
export interface IEntityService<T extends ISync> {
|
||||
/**
|
||||
* Entity type discriminator for runtime routing
|
||||
* Must match EntityType values: 'Event', 'Booking', 'Customer', 'Resource'
|
||||
*/
|
||||
readonly entityType: EntityType;
|
||||
|
||||
/**
|
||||
* Mark entity as successfully synced with backend
|
||||
* Sets syncStatus = 'synced' and persists to IndexedDB
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
markAsSynced(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Mark entity as sync error (max retries exceeded)
|
||||
* Sets syncStatus = 'error' and persists to IndexedDB
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
markAsError(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get current sync status for an entity
|
||||
* Used by SyncManager to check entity state
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns SyncStatus or null if entity not found
|
||||
*/
|
||||
getSyncStatus(id: string): Promise<SyncStatus | null>;
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { IDataEntity } from '../types/CalendarTypes';
|
||||
import { IStore } from './IStore';
|
||||
|
||||
/**
|
||||
* Operation for the sync queue
|
||||
* Generic structure supporting all entity types (Event, Booking, Customer, Resource)
|
||||
*/
|
||||
export interface IQueueOperation {
|
||||
id: string;
|
||||
type: 'create' | 'update' | 'delete';
|
||||
eventId: string;
|
||||
data: Partial<ICalendarEvent> | ICalendarEvent;
|
||||
entityId: string;
|
||||
dataEntity: IDataEntity;
|
||||
timestamp: number;
|
||||
retryCount: number;
|
||||
}
|
||||
|
|
@ -116,7 +117,7 @@ export class IndexedDBService {
|
|||
const db = this.ensureDB();
|
||||
const queueItem: IQueueOperation = {
|
||||
...operation,
|
||||
id: `${operation.type}-${operation.eventId}-${Date.now()}`
|
||||
id: `${operation.type}-${operation.entityId}-${Date.now()}`
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -77,23 +77,37 @@ export class OperationQueue {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get operations for a specific event ID
|
||||
* Get operations for a specific entity ID
|
||||
*/
|
||||
async getOperationsForEvent(eventId: string): Promise<IQueueOperation[]> {
|
||||
async getOperationsForEntity(entityId: string): Promise<IQueueOperation[]> {
|
||||
const queue = await this.getAll();
|
||||
return queue.filter(op => op.eventId === eventId);
|
||||
return queue.filter(op => op.entityId === entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all operations for a specific event ID
|
||||
* Remove all operations for a specific entity ID
|
||||
*/
|
||||
async removeOperationsForEvent(eventId: string): Promise<void> {
|
||||
const operations = await this.getOperationsForEvent(eventId);
|
||||
async removeOperationsForEntity(entityId: string): Promise<void> {
|
||||
const operations = await this.getOperationsForEntity(entityId);
|
||||
for (const op of operations) {
|
||||
await this.remove(op.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getOperationsForEntity instead
|
||||
*/
|
||||
async getOperationsForEvent(eventId: string): Promise<IQueueOperation[]> {
|
||||
return this.getOperationsForEntity(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use removeOperationsForEntity instead
|
||||
*/
|
||||
async removeOperationsForEvent(eventId: string): Promise<void> {
|
||||
return this.removeOperationsForEntity(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update retry count for an operation
|
||||
*/
|
||||
|
|
|
|||
90
src/storage/SyncPlugin.ts
Normal file
90
src/storage/SyncPlugin.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { ISync, SyncStatus, EntityType } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* SyncPlugin<T extends ISync> - Pluggable sync functionality for entity services
|
||||
*
|
||||
* COMPOSITION PATTERN:
|
||||
* - Encapsulates all sync-related logic in separate class
|
||||
* - Composed into BaseEntityService (not inheritance)
|
||||
* - Allows sync functionality to be swapped/mocked for testing
|
||||
* - Single Responsibility: Only handles sync status management
|
||||
*
|
||||
* DESIGN:
|
||||
* - Takes reference to BaseEntityService for calling get/save
|
||||
* - Implements sync methods that delegate to service's CRUD
|
||||
* - Uses IndexedDB syncStatus index for efficient queries
|
||||
*/
|
||||
export class SyncPlugin<T extends ISync> {
|
||||
/**
|
||||
* @param service - Reference to BaseEntityService for CRUD operations
|
||||
*/
|
||||
constructor(private service: any) {
|
||||
// Type is 'any' to avoid circular dependency at compile time
|
||||
// Runtime: service is BaseEntityService<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark entity as successfully synced
|
||||
* Sets syncStatus = 'synced' and persists to IndexedDB
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsSynced(id: string): Promise<void> {
|
||||
const entity = await this.service.get(id);
|
||||
if (entity) {
|
||||
entity.syncStatus = 'synced';
|
||||
await this.service.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark entity as sync error (max retries exceeded)
|
||||
* Sets syncStatus = 'error' and persists to IndexedDB
|
||||
*
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
async markAsError(id: string): Promise<void> {
|
||||
const entity = await this.service.get(id);
|
||||
if (entity) {
|
||||
entity.syncStatus = 'error';
|
||||
await this.service.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current sync status for an entity
|
||||
*
|
||||
* @param id - Entity ID
|
||||
* @returns SyncStatus or null if entity not found
|
||||
*/
|
||||
async getSyncStatus(id: string): Promise<SyncStatus | null> {
|
||||
const entity = await this.service.get(id);
|
||||
return entity ? entity.syncStatus : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entities by sync status
|
||||
* Uses IndexedDB syncStatus index for efficient querying
|
||||
*
|
||||
* @param syncStatus - Sync status ('synced', 'pending', 'error')
|
||||
* @returns Array of entities with this sync status
|
||||
*/
|
||||
async getBySyncStatus(syncStatus: string): Promise<T[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.service.db.transaction([this.service.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.service.storeName);
|
||||
const index = store.index('syncStatus');
|
||||
const request = index.getAll(syncStatus);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result as any[];
|
||||
const entities = data.map(item => this.service.deserialize(item));
|
||||
resolve(entities);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get ${this.service.entityType}s by sync status ${syncStatus}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +1,43 @@
|
|||
import { IBooking } from '../../types/BookingTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { BookingStore } from './BookingStore';
|
||||
import { BookingSerialization } from './BookingSerialization';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ARCHITECTURE:
|
||||
* - Extends BaseEntityService for shared CRUD and sync logic
|
||||
* - Overrides serialize/deserialize for Date field conversion (createdAt)
|
||||
* - Provides booking-specific query methods (by customer, by status)
|
||||
*
|
||||
* INHERITED METHODS (from BaseEntityService):
|
||||
* - get(id), getAll(), save(entity), delete(id)
|
||||
* - markAsSynced(id), markAsError(id), getSyncStatus(id), getBySyncStatus(status)
|
||||
*
|
||||
* BOOKING-SPECIFIC METHODS:
|
||||
* - getByCustomer(customerId)
|
||||
* - getByStatus(status)
|
||||
*/
|
||||
export class BookingService {
|
||||
private db: IDBDatabase;
|
||||
export class BookingService extends BaseEntityService<IBooking> {
|
||||
readonly storeName = BookingStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Booking';
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
* Serialize booking for IndexedDB storage
|
||||
* Converts Date objects to ISO strings
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
protected serialize(booking: IBooking): any {
|
||||
return BookingSerialization.serialize(booking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single booking by ID
|
||||
*
|
||||
* @param id - Booking ID
|
||||
* @returns IBooking or null if not found
|
||||
* Deserialize booking from IndexedDB
|
||||
* Converts ISO strings back to Date objects
|
||||
*/
|
||||
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}`));
|
||||
};
|
||||
});
|
||||
protected deserialize(data: any): IBooking {
|
||||
return BookingSerialization.deserialize(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,14 +48,14 @@ export class BookingService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
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));
|
||||
const bookings = data.map(item => this.deserialize(item));
|
||||
resolve(bookings);
|
||||
};
|
||||
|
||||
|
|
@ -145,14 +73,14 @@ export class BookingService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
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));
|
||||
const bookings = data.map(item => this.deserialize(item));
|
||||
resolve(bookings);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ export class BookingStore implements IStore {
|
|||
// Index: status (for filtering by booking status)
|
||||
store.createIndex('status', 'status', { unique: false });
|
||||
|
||||
// Index: syncStatus (for querying by sync status - used by SyncPlugin)
|
||||
store.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
|
||||
// Index: createdAt (for sorting bookings chronologically)
|
||||
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,108 +1,29 @@
|
|||
import { ICustomer } from '../../types/CustomerTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { CustomerStore } from './CustomerStore';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ARCHITECTURE:
|
||||
* - Extends BaseEntityService for shared CRUD and sync logic
|
||||
* - No serialization needed (ICustomer has no Date fields)
|
||||
* - Provides customer-specific query methods (by phone, search by name)
|
||||
*
|
||||
* Note: No serialization needed - ICustomer has no Date fields.
|
||||
* INHERITED METHODS (from BaseEntityService):
|
||||
* - get(id), getAll(), save(entity), delete(id)
|
||||
* - markAsSynced(id), markAsError(id), getSyncStatus(id), getBySyncStatus(status)
|
||||
*
|
||||
* CUSTOMER-SPECIFIC METHODS:
|
||||
* - getByPhone(phone)
|
||||
* - searchByName(searchTerm)
|
||||
*/
|
||||
export class CustomerService {
|
||||
private db: IDBDatabase;
|
||||
export class CustomerService extends BaseEntityService<ICustomer> {
|
||||
readonly storeName = CustomerStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Customer';
|
||||
|
||||
/**
|
||||
* @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}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
// No serialization override needed - ICustomer has no Date fields
|
||||
|
||||
/**
|
||||
* Get customers by phone number
|
||||
|
|
@ -112,8 +33,8 @@ export class CustomerService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('phone');
|
||||
const request = index.getAll(phone);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ export class CustomerStore implements IStore {
|
|||
|
||||
// Index: phone (for customer lookup by phone)
|
||||
store.createIndex('phone', 'phone', { unique: false });
|
||||
|
||||
// Index: syncStatus (for querying by sync status - used by SyncPlugin)
|
||||
store.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,115 +1,45 @@
|
|||
import { ICalendarEvent } from '../../types/CalendarTypes';
|
||||
import { ICalendarEvent, EntityType } from '../../types/CalendarTypes';
|
||||
import { EventStore } from './EventStore';
|
||||
import { EventSerialization } from './EventSerialization';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ARCHITECTURE:
|
||||
* - Extends BaseEntityService for shared CRUD and sync logic
|
||||
* - Overrides serialize/deserialize for Date field conversion
|
||||
* - Provides event-specific query methods (by date range, resource, customer, booking)
|
||||
*
|
||||
* INHERITED METHODS (from BaseEntityService):
|
||||
* - get(id), getAll(), save(entity), delete(id)
|
||||
* - markAsSynced(id), markAsError(id), getSyncStatus(id), getBySyncStatus(status)
|
||||
*
|
||||
* EVENT-SPECIFIC METHODS:
|
||||
* - getByDateRange(start, end)
|
||||
* - getByResource(resourceId)
|
||||
* - getByCustomer(customerId)
|
||||
* - getByBooking(bookingId)
|
||||
* - getByResourceAndDateRange(resourceId, start, end)
|
||||
*/
|
||||
export class EventService {
|
||||
private db: IDBDatabase;
|
||||
export class EventService extends BaseEntityService<ICalendarEvent> {
|
||||
readonly storeName = EventStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Event';
|
||||
|
||||
/**
|
||||
* @param db - IDBDatabase instance (injected dependency)
|
||||
* Serialize event for IndexedDB storage
|
||||
* Converts Date objects to ISO strings
|
||||
*/
|
||||
constructor(db: IDBDatabase) {
|
||||
this.db = db;
|
||||
protected serialize(event: ICalendarEvent): any {
|
||||
return EventSerialization.serialize(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by ID
|
||||
*
|
||||
* @param id - Event ID
|
||||
* @returns ICalendarEvent or null if not found
|
||||
* Deserialize event from IndexedDB
|
||||
* Converts ISO strings back to Date objects
|
||||
*/
|
||||
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}`));
|
||||
};
|
||||
});
|
||||
protected deserialize(data: any): ICalendarEvent {
|
||||
return EventSerialization.deserialize(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -122,8 +52,8 @@ export class EventService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('start');
|
||||
|
||||
// Get all events starting from start date
|
||||
|
|
@ -135,7 +65,7 @@ export class EventService {
|
|||
|
||||
// Deserialize and filter in memory
|
||||
const events = data
|
||||
.map(item => EventSerialization.deserialize(item))
|
||||
.map(item => this.deserialize(item))
|
||||
.filter(event => event.start <= end);
|
||||
|
||||
resolve(events);
|
||||
|
|
@ -155,14 +85,14 @@ export class EventService {
|
|||
*/
|
||||
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 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 any[];
|
||||
const events = data.map(item => EventSerialization.deserialize(item));
|
||||
const events = data.map(item => this.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
|
|
@ -180,14 +110,14 @@ export class EventService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
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));
|
||||
const events = data.map(item => this.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
|
|
@ -205,14 +135,14 @@ export class EventService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
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));
|
||||
const events = data.map(item => this.deserialize(item));
|
||||
resolve(events);
|
||||
};
|
||||
|
||||
|
|
@ -222,31 +152,6 @@ export class EventService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -1,108 +1,30 @@
|
|||
import { IResource } from '../../types/ResourceTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { ResourceStore } from './ResourceStore';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ARCHITECTURE:
|
||||
* - Extends BaseEntityService for shared CRUD and sync logic
|
||||
* - No serialization needed (IResource has no Date fields)
|
||||
* - Provides resource-specific query methods (by type, active/inactive)
|
||||
*
|
||||
* Note: No serialization needed - IResource has no Date fields.
|
||||
* INHERITED METHODS (from BaseEntityService):
|
||||
* - get(id), getAll(), save(entity), delete(id)
|
||||
* - markAsSynced(id), markAsError(id), getSyncStatus(id), getBySyncStatus(status)
|
||||
*
|
||||
* RESOURCE-SPECIFIC METHODS:
|
||||
* - getByType(type)
|
||||
* - getActive()
|
||||
* - getInactive()
|
||||
*/
|
||||
export class ResourceService {
|
||||
private db: IDBDatabase;
|
||||
export class ResourceService extends BaseEntityService<IResource> {
|
||||
readonly storeName = ResourceStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Resource';
|
||||
|
||||
/**
|
||||
* @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}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
// No serialization override needed - IResource has no Date fields
|
||||
|
||||
/**
|
||||
* Get resources by type
|
||||
|
|
@ -112,8 +34,8 @@ export class ResourceService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('type');
|
||||
const request = index.getAll(type);
|
||||
|
||||
|
|
@ -134,10 +56,10 @@ export class ResourceService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('isActive');
|
||||
const request = index.getAll(true);
|
||||
const request = index.getAll(IDBKeyRange.only(true));
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
|
|
@ -156,10 +78,10 @@ export class ResourceService {
|
|||
*/
|
||||
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 transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('isActive');
|
||||
const request = index.getAll(false);
|
||||
const request = index.getAll(IDBKeyRange.only(false));
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IResource[]);
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ export class ResourceStore implements IStore {
|
|||
|
||||
// Index: isActive (for showing/hiding inactive resources)
|
||||
store.createIndex('isActive', 'isActive', { unique: false });
|
||||
|
||||
// Index: syncStatus (for querying by sync status - used by SyncPlugin)
|
||||
store.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue