Adds audit logging and sync management infrastructure
Introduces comprehensive audit trail system with: - AuditService to track entity changes - SyncManager for background sync of audit entries - New CoreEvents for entity and audit tracking - Simplified sync architecture with event-driven approach Prepares system for enhanced compliance and change tracking
This commit is contained in:
parent
dcd76836bd
commit
9ea98e3a04
18 changed files with 469 additions and 414 deletions
|
|
@ -2,6 +2,9 @@ import { ISync, EntityType, SyncStatus } from '../types/CalendarTypes';
|
|||
import { IEntityService } from './IEntityService';
|
||||
import { SyncPlugin } from './SyncPlugin';
|
||||
import { IndexedDBContext } from './IndexedDBContext';
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { diff } from 'json-diff-ts';
|
||||
|
||||
/**
|
||||
* BaseEntityService<T extends ISync> - Abstract base class for all entity services
|
||||
|
|
@ -42,11 +45,16 @@ export abstract class BaseEntityService<T extends ISync> implements IEntityServi
|
|||
// IndexedDB context - provides database connection
|
||||
private context: IndexedDBContext;
|
||||
|
||||
// EventBus for emitting entity events
|
||||
protected eventBus: IEventBus;
|
||||
|
||||
/**
|
||||
* @param context - IndexedDBContext instance (injected dependency)
|
||||
* @param eventBus - EventBus for emitting entity events
|
||||
*/
|
||||
constructor(context: IndexedDBContext) {
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
this.context = context;
|
||||
this.eventBus = eventBus;
|
||||
this.syncPlugin = new SyncPlugin<T>(this);
|
||||
}
|
||||
|
||||
|
|
@ -132,10 +140,28 @@ export abstract class BaseEntityService<T extends ISync> implements IEntityServi
|
|||
|
||||
/**
|
||||
* Save an entity (create or update)
|
||||
* Emits ENTITY_SAVED event with operation type and changes
|
||||
*
|
||||
* @param entity - Entity to save
|
||||
*/
|
||||
async save(entity: T): Promise<void> {
|
||||
const entityId = (entity as any).id;
|
||||
|
||||
// Check if entity exists to determine create vs update
|
||||
const existingEntity = await this.get(entityId);
|
||||
const isCreate = existingEntity === null;
|
||||
|
||||
// Calculate changes: full entity for create, diff for update
|
||||
let changes: any;
|
||||
if (isCreate) {
|
||||
changes = entity;
|
||||
} else {
|
||||
// Calculate diff between existing and new entity
|
||||
const existingSerialized = this.serialize(existingEntity);
|
||||
const newSerialized = this.serialize(entity);
|
||||
changes = diff(existingSerialized, newSerialized);
|
||||
}
|
||||
|
||||
const serialized = this.serialize(entity);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -144,17 +170,26 @@ export abstract class BaseEntityService<T extends ISync> implements IEntityServi
|
|||
const request = store.put(serialized);
|
||||
|
||||
request.onsuccess = () => {
|
||||
// Emit ENTITY_SAVED event
|
||||
this.eventBus.emit(CoreEvents.ENTITY_SAVED, {
|
||||
entityType: this.entityType,
|
||||
entityId,
|
||||
operation: isCreate ? 'create' : 'update',
|
||||
changes,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save ${this.entityType} ${(entity as any).id}: ${request.error}`));
|
||||
reject(new Error(`Failed to save ${this.entityType} ${entityId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entity
|
||||
* Emits ENTITY_DELETED event
|
||||
*
|
||||
* @param id - Entity ID to delete
|
||||
*/
|
||||
|
|
@ -165,6 +200,13 @@ export abstract class BaseEntityService<T extends ISync> implements IEntityServi
|
|||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
// Emit ENTITY_DELETED event
|
||||
this.eventBus.emit(CoreEvents.ENTITY_DELETED, {
|
||||
entityType: this.entityType,
|
||||
entityId: id,
|
||||
operation: 'delete',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { IStore } from './IStore';
|
|||
*/
|
||||
export class IndexedDBContext {
|
||||
private static readonly DB_NAME = 'CalendarDB';
|
||||
private static readonly DB_VERSION = 2;
|
||||
private static readonly DB_VERSION = 3; // Bumped for audit store
|
||||
static readonly QUEUE_STORE = 'operationQueue';
|
||||
static readonly SYNC_STATE_STORE = 'syncState';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,263 +0,0 @@
|
|||
import { IndexedDBContext } from './IndexedDBContext';
|
||||
import { IDataEntity } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* Operation for the sync queue
|
||||
* Generic structure supporting all entity types (Event, Booking, Customer, Resource)
|
||||
*/
|
||||
export interface IQueueOperation {
|
||||
id: string;
|
||||
type: 'create' | 'update' | 'delete';
|
||||
entityId: string;
|
||||
dataEntity: IDataEntity;
|
||||
timestamp: number;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation Queue Manager
|
||||
* Handles FIFO queue of pending sync operations and sync state metadata
|
||||
*
|
||||
* RESPONSIBILITY:
|
||||
* - Queue operations (enqueue, dequeue, peek, clear)
|
||||
* - Sync state management (setSyncState, getSyncState)
|
||||
* - Direct IndexedDB operations on queue and syncState stores
|
||||
*
|
||||
* ARCHITECTURE:
|
||||
* - Moved from IndexedDBService to achieve better separation of concerns
|
||||
* - IndexedDBContext provides database connection
|
||||
* - OperationQueue owns queue business logic
|
||||
*/
|
||||
export class OperationQueue {
|
||||
private context: IndexedDBContext;
|
||||
|
||||
constructor(context: IndexedDBContext) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Queue Operations
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Add operation to the end of the queue
|
||||
*/
|
||||
async enqueue(operation: Omit<IQueueOperation, 'id'>): Promise<void> {
|
||||
const db = this.context.getDatabase();
|
||||
const queueItem: IQueueOperation = {
|
||||
...operation,
|
||||
id: `${operation.type}-${operation.entityId}-${Date.now()}`
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBContext.QUEUE_STORE);
|
||||
const request = store.put(queueItem);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to add to queue: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all operations in the queue (sorted by timestamp FIFO)
|
||||
*/
|
||||
async getAll(): Promise<IQueueOperation[]> {
|
||||
const db = this.context.getDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.QUEUE_STORE], 'readonly');
|
||||
const store = transaction.objectStore(IndexedDBContext.QUEUE_STORE);
|
||||
const index = store.index('timestamp');
|
||||
const request = index.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result as IQueueOperation[]);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get queue: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first operation from the queue (without removing it)
|
||||
* Returns null if queue is empty
|
||||
*/
|
||||
async peek(): Promise<IQueueOperation | null> {
|
||||
const queue = await this.getAll();
|
||||
return queue.length > 0 ? queue[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific operation from the queue
|
||||
*/
|
||||
async remove(operationId: string): Promise<void> {
|
||||
const db = this.context.getDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBContext.QUEUE_STORE);
|
||||
const request = store.delete(operationId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to remove from queue: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the first operation from the queue and return it
|
||||
* Returns null if queue is empty
|
||||
*/
|
||||
async dequeue(): Promise<IQueueOperation | null> {
|
||||
const operation = await this.peek();
|
||||
if (operation) {
|
||||
await this.remove(operation.id);
|
||||
}
|
||||
return operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all operations from the queue
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
const db = this.context.getDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBContext.QUEUE_STORE);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to clear queue: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of operations in the queue
|
||||
*/
|
||||
async size(): Promise<number> {
|
||||
const queue = await this.getAll();
|
||||
return queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
*/
|
||||
async isEmpty(): Promise<boolean> {
|
||||
const size = await this.size();
|
||||
return size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operations for a specific entity ID
|
||||
*/
|
||||
async getOperationsForEntity(entityId: string): Promise<IQueueOperation[]> {
|
||||
const queue = await this.getAll();
|
||||
return queue.filter(op => op.entityId === entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all operations for a specific entity ID
|
||||
*/
|
||||
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
|
||||
*/
|
||||
async incrementRetryCount(operationId: string): Promise<void> {
|
||||
const queue = await this.getAll();
|
||||
const operation = queue.find(op => op.id === operationId);
|
||||
|
||||
if (operation) {
|
||||
operation.retryCount++;
|
||||
// Re-add to queue with updated retry count
|
||||
await this.remove(operationId);
|
||||
await this.enqueue(operation);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Sync State Operations
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save sync state value
|
||||
* Used to store sync metadata like lastSyncTime, etc.
|
||||
*
|
||||
* @param key - State key
|
||||
* @param value - State value (any serializable data)
|
||||
*/
|
||||
async setSyncState(key: string, value: any): Promise<void> {
|
||||
const db = this.context.getDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.SYNC_STATE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(IndexedDBContext.SYNC_STATE_STORE);
|
||||
const request = store.put({ key, value });
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to set sync state ${key}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync state value
|
||||
*
|
||||
* @param key - State key
|
||||
* @returns State value or null if not found
|
||||
*/
|
||||
async getSyncState(key: string): Promise<any | null> {
|
||||
const db = this.context.getDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([IndexedDBContext.SYNC_STATE_STORE], 'readonly');
|
||||
const store = transaction.objectStore(IndexedDBContext.SYNC_STATE_STORE);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result;
|
||||
resolve(result ? result.value : null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get sync state ${key}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
177
src/storage/audit/AuditService.ts
Normal file
177
src/storage/audit/AuditService.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
import { IAuditEntry } from '../../types/AuditTypes';
|
||||
import { EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { CoreEvents } from '../../constants/CoreEvents';
|
||||
|
||||
/**
|
||||
* AuditService - Entity service for audit entries
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Store audit entries in IndexedDB
|
||||
* - Listen for ENTITY_SAVED/ENTITY_DELETED events
|
||||
* - Create audit entries for all entity changes
|
||||
* - Emit AUDIT_LOGGED after saving (for SyncManager to listen)
|
||||
*
|
||||
* OVERRIDE PATTERN:
|
||||
* - Overrides save() to NOT emit events (prevents infinite loops)
|
||||
* - AuditService saves audit entries without triggering more audits
|
||||
*
|
||||
* EVENT CHAIN:
|
||||
* Entity change → ENTITY_SAVED/DELETED → AuditService → AUDIT_LOGGED → SyncManager
|
||||
*/
|
||||
export class AuditService extends BaseEntityService<IAuditEntry> {
|
||||
readonly storeName = 'audit';
|
||||
readonly entityType: EntityType = 'Audit';
|
||||
|
||||
// Hardcoded userId for now - will come from session later
|
||||
private static readonly DEFAULT_USER_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for ENTITY_SAVED and ENTITY_DELETED events
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
// Listen for entity saves (create/update)
|
||||
this.eventBus.on(CoreEvents.ENTITY_SAVED, (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
this.handleEntitySaved(detail);
|
||||
});
|
||||
|
||||
// Listen for entity deletes
|
||||
this.eventBus.on(CoreEvents.ENTITY_DELETED, (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
this.handleEntityDeleted(detail);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ENTITY_SAVED event - create audit entry
|
||||
*/
|
||||
private async handleEntitySaved(payload: {
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
operation: 'create' | 'update';
|
||||
changes: any;
|
||||
timestamp: number;
|
||||
}): Promise<void> {
|
||||
// Don't audit audit entries (prevent infinite loops)
|
||||
if (payload.entityType === 'Audit') return;
|
||||
|
||||
const auditEntry: IAuditEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
entityType: payload.entityType,
|
||||
entityId: payload.entityId,
|
||||
operation: payload.operation,
|
||||
userId: AuditService.DEFAULT_USER_ID,
|
||||
timestamp: payload.timestamp,
|
||||
changes: payload.changes,
|
||||
synced: false,
|
||||
syncStatus: 'pending'
|
||||
};
|
||||
|
||||
await this.save(auditEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ENTITY_DELETED event - create audit entry
|
||||
*/
|
||||
private async handleEntityDeleted(payload: {
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
operation: 'delete';
|
||||
timestamp: number;
|
||||
}): Promise<void> {
|
||||
// Don't audit audit entries (prevent infinite loops)
|
||||
if (payload.entityType === 'Audit') return;
|
||||
|
||||
const auditEntry: IAuditEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
entityType: payload.entityType,
|
||||
entityId: payload.entityId,
|
||||
operation: 'delete',
|
||||
userId: AuditService.DEFAULT_USER_ID,
|
||||
timestamp: payload.timestamp,
|
||||
changes: { id: payload.entityId }, // For delete, just store the ID
|
||||
synced: false,
|
||||
syncStatus: 'pending'
|
||||
};
|
||||
|
||||
await this.save(auditEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override save to NOT trigger ENTITY_SAVED event
|
||||
* Instead, emits AUDIT_LOGGED for SyncManager to listen
|
||||
*
|
||||
* This prevents infinite loops:
|
||||
* - BaseEntityService.save() emits ENTITY_SAVED
|
||||
* - AuditService listens to ENTITY_SAVED and creates audit
|
||||
* - If AuditService.save() also emitted ENTITY_SAVED, it would loop
|
||||
*/
|
||||
async save(entity: IAuditEntry): 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 = () => {
|
||||
// Emit AUDIT_LOGGED instead of ENTITY_SAVED
|
||||
this.eventBus.emit(CoreEvents.AUDIT_LOGGED, {
|
||||
auditId: entity.id,
|
||||
entityType: entity.entityType,
|
||||
entityId: entity.entityId,
|
||||
operation: entity.operation,
|
||||
timestamp: entity.timestamp
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to save audit entry ${entity.id}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override delete to NOT trigger ENTITY_DELETED event
|
||||
* Audit entries should never be deleted (compliance requirement)
|
||||
*/
|
||||
async delete(_id: string): Promise<void> {
|
||||
throw new Error('Audit entries cannot be deleted (compliance requirement)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending audit entries (for sync)
|
||||
*/
|
||||
async getPendingAudits(): Promise<IAuditEntry[]> {
|
||||
return this.getBySyncStatus('pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit entries for a specific entity
|
||||
*/
|
||||
async getByEntityId(entityId: string): Promise<IAuditEntry[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('entityId');
|
||||
const request = index.getAll(entityId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const entries = request.result as IAuditEntry[];
|
||||
resolve(entries);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to get audit entries for entity ${entityId}: ${request.error}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/storage/audit/AuditStore.ts
Normal file
25
src/storage/audit/AuditStore.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { IStore } from '../IStore';
|
||||
|
||||
/**
|
||||
* AuditStore - IndexedDB store configuration for audit entries
|
||||
*
|
||||
* Stores all entity changes for:
|
||||
* - Compliance and audit trail
|
||||
* - Sync tracking with backend
|
||||
* - Change history
|
||||
*
|
||||
* Indexes:
|
||||
* - syncStatus: For finding pending entries to sync
|
||||
* - synced: Boolean flag for quick sync queries
|
||||
*/
|
||||
export class AuditStore implements IStore {
|
||||
readonly storeName = 'audit';
|
||||
|
||||
create(db: IDBDatabase): void {
|
||||
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
store.createIndex('synced', 'synced', { unique: false });
|
||||
store.createIndex('entityId', 'entityId', { unique: false });
|
||||
store.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { IBooking } from '../../types/BookingTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { BookingStore } from './BookingStore';
|
||||
import { BookingSerialization } from './BookingSerialization';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
|
||||
/**
|
||||
* BookingService - CRUD operations for bookings in IndexedDB
|
||||
|
|
@ -24,6 +25,10 @@ export class BookingService extends BaseEntityService<IBooking> {
|
|||
readonly storeName = BookingStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Booking';
|
||||
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize booking for IndexedDB storage
|
||||
* Converts Date objects to ISO strings
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ICustomer } from '../../types/CustomerTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { CustomerStore } from './CustomerStore';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
|
||||
/**
|
||||
* CustomerService - CRUD operations for customers in IndexedDB
|
||||
|
|
@ -23,7 +24,9 @@ export class CustomerService extends BaseEntityService<ICustomer> {
|
|||
readonly storeName = CustomerStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Customer';
|
||||
|
||||
// No serialization override needed - ICustomer has no Date fields
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customers by phone number
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { ICalendarEvent, EntityType } from '../../types/CalendarTypes';
|
||||
import { ICalendarEvent, EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { EventStore } from './EventStore';
|
||||
import { EventSerialization } from './EventSerialization';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
|
||||
/**
|
||||
* EventService - CRUD operations for calendar events in IndexedDB
|
||||
|
|
@ -26,6 +27,10 @@ export class EventService extends BaseEntityService<ICalendarEvent> {
|
|||
readonly storeName = EventStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Event';
|
||||
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize event for IndexedDB storage
|
||||
* Converts Date objects to ISO strings
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { IResource } from '../../types/ResourceTypes';
|
||||
import { EntityType } from '../../types/CalendarTypes';
|
||||
import { EntityType, IEventBus } from '../../types/CalendarTypes';
|
||||
import { ResourceStore } from './ResourceStore';
|
||||
import { BaseEntityService } from '../BaseEntityService';
|
||||
import { IndexedDBContext } from '../IndexedDBContext';
|
||||
|
||||
/**
|
||||
* ResourceService - CRUD operations for resources in IndexedDB
|
||||
|
|
@ -24,7 +25,9 @@ export class ResourceService extends BaseEntityService<IResource> {
|
|||
readonly storeName = ResourceStore.STORE_NAME;
|
||||
readonly entityType: EntityType = 'Resource';
|
||||
|
||||
// No serialization override needed - IResource has no Date fields
|
||||
constructor(context: IndexedDBContext, eventBus: IEventBus) {
|
||||
super(context, eventBus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resources by type
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue