Adds comprehensive mock data repositories and seeding infrastructure
Implements polymorphic data seeding mechanism for initial application setup - Adds Mock repositories for Event, Booking, Customer, and Resource entities - Creates DataSeeder to automatically populate IndexedDB from JSON sources - Enhances index.ts initialization process with data seeding step - Adds mock JSON data files for comprehensive test data Improves offline-first and development testing capabilities
This commit is contained in:
parent
871f5c5682
commit
5648c7c304
11 changed files with 1641 additions and 40 deletions
39
src/index.ts
39
src/index.ts
|
|
@ -4,7 +4,7 @@ import { eventBus } from './core/EventBus';
|
|||
import { ConfigManager } from './configurations/ConfigManager';
|
||||
import { Configuration } from './configurations/CalendarConfig';
|
||||
import { URLManager } from './utils/URLManager';
|
||||
import { IEventBus } from './types/CalendarTypes';
|
||||
import { ICalendarEvent, IEventBus } from './types/CalendarTypes';
|
||||
|
||||
// Import all managers
|
||||
import { EventManager } from './managers/EventManager';
|
||||
|
|
@ -25,6 +25,9 @@ import { WorkweekPresets } from './components/WorkweekPresets';
|
|||
// Import repositories and storage
|
||||
import { IEventRepository } from './repositories/IEventRepository';
|
||||
import { MockEventRepository } from './repositories/MockEventRepository';
|
||||
import { MockBookingRepository } from './repositories/MockBookingRepository';
|
||||
import { MockCustomerRepository } from './repositories/MockCustomerRepository';
|
||||
import { MockResourceRepository } from './repositories/MockResourceRepository';
|
||||
import { IndexedDBEventRepository } from './repositories/IndexedDBEventRepository';
|
||||
import { IApiRepository } from './repositories/IApiRepository';
|
||||
import { ApiEventRepository } from './repositories/ApiEventRepository';
|
||||
|
|
@ -46,6 +49,7 @@ import { ResourceService } from './storage/resources/ResourceService';
|
|||
|
||||
// Import workers
|
||||
import { SyncManager } from './workers/SyncManager';
|
||||
import { DataSeeder } from './workers/DataSeeder';
|
||||
|
||||
// Import renderers
|
||||
import { DateHeaderRenderer, type IHeaderRenderer } from './renderers/DateHeaderRenderer';
|
||||
|
|
@ -65,6 +69,9 @@ import { EventStackManager } from './managers/EventStackManager';
|
|||
import { EventLayoutCoordinator } from './managers/EventLayoutCoordinator';
|
||||
import { IColumnDataSource } from './types/ColumnDataSource';
|
||||
import { DateColumnDataSource } from './datasources/DateColumnDataSource';
|
||||
import { IBooking } from './types/BookingTypes';
|
||||
import { ICustomer } from './types/CustomerTypes';
|
||||
import { IResource } from './types/ResourceTypes';
|
||||
|
||||
/**
|
||||
* Handle deep linking functionality after managers are initialized
|
||||
|
|
@ -122,26 +129,27 @@ async function initializeCalendar(): Promise<void> {
|
|||
builder.registerType(IndexedDBService).as<IndexedDBService>();
|
||||
builder.registerType(OperationQueue).as<OperationQueue>();
|
||||
|
||||
// Register API repositories (backend sync)
|
||||
// Each entity type has its own API repository implementing IApiRepository<T>
|
||||
builder.registerType(ApiEventRepository).as<IApiRepository<any>>();
|
||||
builder.registerType(ApiBookingRepository).as<IApiRepository<any>>();
|
||||
builder.registerType(ApiCustomerRepository).as<IApiRepository<any>>();
|
||||
builder.registerType(ApiResourceRepository).as<IApiRepository<any>>();
|
||||
// Register Mock repositories (development/testing - load from JSON files)
|
||||
// Each entity type has its own Mock repository implementing IApiRepository<T>
|
||||
builder.registerType(MockEventRepository).as<IApiRepository<ICalendarEvent>>();
|
||||
builder.registerType(MockBookingRepository).as<IApiRepository<IBooking>>();
|
||||
builder.registerType(MockCustomerRepository).as<IApiRepository<ICustomer>>();
|
||||
builder.registerType(MockResourceRepository).as<IApiRepository<IResource>>();
|
||||
|
||||
builder.registerType(DateColumnDataSource).as<IColumnDataSource>();
|
||||
// Register entity services (sync status management)
|
||||
// Register entity services (sync status management)
|
||||
// Open/Closed Principle: Adding new entity only requires adding one line here
|
||||
builder.registerType(EventService).as<IEntityService<any>>();
|
||||
builder.registerType(BookingService).as<IEntityService<any>>();
|
||||
builder.registerType(CustomerService).as<IEntityService<any>>();
|
||||
builder.registerType(ResourceService).as<IEntityService<any>>();
|
||||
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
|
||||
builder.registerType(BookingService).as<IEntityService<IBooking>>();
|
||||
builder.registerType(CustomerService).as<IEntityService<ICustomer>>();
|
||||
builder.registerType(ResourceService).as<IEntityService<IResource>>();
|
||||
|
||||
// Register IndexedDB repositories (offline-first)
|
||||
builder.registerType(IndexedDBEventRepository).as<IEventRepository>();
|
||||
|
||||
// Register workers
|
||||
builder.registerType(SyncManager).as<SyncManager>();
|
||||
builder.registerType(DataSeeder).as<DataSeeder>();
|
||||
|
||||
// Register renderers
|
||||
builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>();
|
||||
|
|
@ -181,6 +189,13 @@ async function initializeCalendar(): Promise<void> {
|
|||
// Build the container
|
||||
const app = builder.build();
|
||||
|
||||
// Initialize database and seed data BEFORE initializing managers
|
||||
const indexedDBService = app.resolveType<IndexedDBService>();
|
||||
await indexedDBService.initialize();
|
||||
|
||||
const dataSeeder = app.resolveType<DataSeeder>();
|
||||
await dataSeeder.seedIfEmpty();
|
||||
|
||||
// Get managers from container
|
||||
const eb = app.resolveType<IEventBus>();
|
||||
const calendarManager = app.resolveType<CalendarManager>();
|
||||
|
|
|
|||
90
src/repositories/MockBookingRepository.ts
Normal file
90
src/repositories/MockBookingRepository.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { IBooking, IBookingService, BookingStatus } from '../types/BookingTypes';
|
||||
import { EntityType } from '../types/CalendarTypes';
|
||||
import { IApiRepository } from './IApiRepository';
|
||||
|
||||
interface RawBookingData {
|
||||
id: string;
|
||||
customerId: string;
|
||||
status: string;
|
||||
createdAt: string | Date;
|
||||
services: RawBookingService[];
|
||||
totalPrice?: number;
|
||||
tags?: string[];
|
||||
notes?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface RawBookingService {
|
||||
serviceId: string;
|
||||
serviceName: string;
|
||||
baseDuration: number;
|
||||
basePrice: number;
|
||||
customPrice?: number;
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MockBookingRepository - Loads booking data from local JSON file
|
||||
*
|
||||
* This repository implementation fetches mock booking data from a static JSON file.
|
||||
* Used for development and testing instead of API calls.
|
||||
*
|
||||
* Data Source: data/mock-bookings.json
|
||||
*
|
||||
* NOTE: Create/Update/Delete operations are not supported - throws errors.
|
||||
* Only fetchAll() is implemented for loading initial mock data.
|
||||
*/
|
||||
export class MockBookingRepository implements IApiRepository<IBooking> {
|
||||
public readonly entityType: EntityType = 'Booking';
|
||||
private readonly dataUrl = 'data/mock-bookings.json';
|
||||
|
||||
/**
|
||||
* Fetch all bookings from mock JSON file
|
||||
*/
|
||||
public async fetchAll(): Promise<IBooking[]> {
|
||||
try {
|
||||
const response = await fetch(this.dataUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load mock bookings: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData: RawBookingData[] = await response.json();
|
||||
|
||||
return this.processBookingData(rawData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load booking data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockBookingRepository is read-only
|
||||
*/
|
||||
public async sendCreate(booking: IBooking): Promise<IBooking> {
|
||||
throw new Error('MockBookingRepository does not support sendCreate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockBookingRepository is read-only
|
||||
*/
|
||||
public async sendUpdate(id: string, updates: Partial<IBooking>): Promise<IBooking> {
|
||||
throw new Error('MockBookingRepository does not support sendUpdate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockBookingRepository is read-only
|
||||
*/
|
||||
public async sendDelete(id: string): Promise<void> {
|
||||
throw new Error('MockBookingRepository does not support sendDelete. Mock data is read-only.');
|
||||
}
|
||||
|
||||
private processBookingData(data: RawBookingData[]): IBooking[] {
|
||||
return data.map((booking): IBooking => ({
|
||||
...booking,
|
||||
createdAt: new Date(booking.createdAt),
|
||||
status: booking.status as BookingStatus,
|
||||
syncStatus: 'synced' as const
|
||||
}));
|
||||
}
|
||||
}
|
||||
76
src/repositories/MockCustomerRepository.ts
Normal file
76
src/repositories/MockCustomerRepository.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { ICustomer } from '../types/CustomerTypes';
|
||||
import { EntityType } from '../types/CalendarTypes';
|
||||
import { IApiRepository } from './IApiRepository';
|
||||
|
||||
interface RawCustomerData {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
metadata?: Record<string, any>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* MockCustomerRepository - Loads customer data from local JSON file
|
||||
*
|
||||
* This repository implementation fetches mock customer data from a static JSON file.
|
||||
* Used for development and testing instead of API calls.
|
||||
*
|
||||
* Data Source: data/mock-customers.json
|
||||
*
|
||||
* NOTE: Create/Update/Delete operations are not supported - throws errors.
|
||||
* Only fetchAll() is implemented for loading initial mock data.
|
||||
*/
|
||||
export class MockCustomerRepository implements IApiRepository<ICustomer> {
|
||||
public readonly entityType: EntityType = 'Customer';
|
||||
private readonly dataUrl = 'data/mock-customers.json';
|
||||
|
||||
/**
|
||||
* Fetch all customers from mock JSON file
|
||||
*/
|
||||
public async fetchAll(): Promise<ICustomer[]> {
|
||||
try {
|
||||
const response = await fetch(this.dataUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load mock customers: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData: RawCustomerData[] = await response.json();
|
||||
|
||||
return this.processCustomerData(rawData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load customer data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockCustomerRepository is read-only
|
||||
*/
|
||||
public async sendCreate(customer: ICustomer): Promise<ICustomer> {
|
||||
throw new Error('MockCustomerRepository does not support sendCreate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockCustomerRepository is read-only
|
||||
*/
|
||||
public async sendUpdate(id: string, updates: Partial<ICustomer>): Promise<ICustomer> {
|
||||
throw new Error('MockCustomerRepository does not support sendUpdate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockCustomerRepository is read-only
|
||||
*/
|
||||
public async sendDelete(id: string): Promise<void> {
|
||||
throw new Error('MockCustomerRepository does not support sendDelete. Mock data is read-only.');
|
||||
}
|
||||
|
||||
private processCustomerData(data: RawCustomerData[]): ICustomer[] {
|
||||
return data.map((customer): ICustomer => ({
|
||||
...customer,
|
||||
syncStatus: 'synced' as const
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +1,50 @@
|
|||
import { ICalendarEvent } from '../types/CalendarTypes';
|
||||
import { ICalendarEvent, EntityType } from '../types/CalendarTypes';
|
||||
import { CalendarEventType } from '../types/BookingTypes';
|
||||
import { IEventRepository, UpdateSource } from './IEventRepository';
|
||||
import { IApiRepository } from './IApiRepository';
|
||||
|
||||
interface RawEventData {
|
||||
// Core fields (required)
|
||||
id: string;
|
||||
title: string;
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
type: string;
|
||||
color?: string;
|
||||
allDay?: boolean;
|
||||
|
||||
// Denormalized references (CRITICAL for booking architecture)
|
||||
bookingId?: string; // Reference to booking (customer events only)
|
||||
resourceId?: string; // Which resource owns this slot
|
||||
customerId?: string; // Customer reference (denormalized from booking)
|
||||
|
||||
// Optional fields
|
||||
description?: string; // Detailed event notes
|
||||
recurringId?: string; // For recurring events
|
||||
metadata?: Record<string, any>; // Flexible metadata
|
||||
|
||||
// Legacy (deprecated, keep for backward compatibility)
|
||||
color?: string; // UI-specific field
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* MockEventRepository - Loads event data from local JSON file (LEGACY)
|
||||
* MockEventRepository - Loads event data from local JSON file
|
||||
*
|
||||
* This repository implementation fetches mock event data from a static JSON file.
|
||||
* DEPRECATED: Use IndexedDBEventRepository for offline-first functionality.
|
||||
* Used for development and testing instead of API calls.
|
||||
*
|
||||
* Data Source: data/mock-events.json
|
||||
*
|
||||
* NOTE: Create/Update/Delete operations are not supported - throws errors.
|
||||
* This is intentional to encourage migration to IndexedDBEventRepository.
|
||||
* Only fetchAll() is implemented for loading initial mock data.
|
||||
*/
|
||||
export class MockEventRepository implements IEventRepository {
|
||||
export class MockEventRepository implements IApiRepository<ICalendarEvent> {
|
||||
public readonly entityType: EntityType = 'Event';
|
||||
private readonly dataUrl = 'data/mock-events.json';
|
||||
|
||||
public async loadEvents(): Promise<ICalendarEvent[]> {
|
||||
/**
|
||||
* Fetch all events from mock JSON file
|
||||
*/
|
||||
public async fetchAll(): Promise<ICalendarEvent[]> {
|
||||
try {
|
||||
const response = await fetch(this.dataUrl);
|
||||
|
||||
|
|
@ -46,36 +63,60 @@ export class MockEventRepository implements IEventRepository {
|
|||
|
||||
/**
|
||||
* NOT SUPPORTED - MockEventRepository is read-only
|
||||
* Use IndexedDBEventRepository instead
|
||||
*/
|
||||
public async createEvent(event: Omit<ICalendarEvent, 'id'>, source?: UpdateSource): Promise<ICalendarEvent> {
|
||||
throw new Error('MockEventRepository does not support createEvent. Use IndexedDBEventRepository instead.');
|
||||
public async sendCreate(event: ICalendarEvent): Promise<ICalendarEvent> {
|
||||
throw new Error('MockEventRepository does not support sendCreate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockEventRepository is read-only
|
||||
* Use IndexedDBEventRepository instead
|
||||
*/
|
||||
public async updateEvent(id: string, updates: Partial<ICalendarEvent>, source?: UpdateSource): Promise<ICalendarEvent> {
|
||||
throw new Error('MockEventRepository does not support updateEvent. Use IndexedDBEventRepository instead.');
|
||||
public async sendUpdate(id: string, updates: Partial<ICalendarEvent>): Promise<ICalendarEvent> {
|
||||
throw new Error('MockEventRepository does not support sendUpdate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockEventRepository is read-only
|
||||
* Use IndexedDBEventRepository instead
|
||||
*/
|
||||
public async deleteEvent(id: string, source?: UpdateSource): Promise<void> {
|
||||
throw new Error('MockEventRepository does not support deleteEvent. Use IndexedDBEventRepository instead.');
|
||||
public async sendDelete(id: string): Promise<void> {
|
||||
throw new Error('MockEventRepository does not support sendDelete. Mock data is read-only.');
|
||||
}
|
||||
|
||||
private processCalendarData(data: RawEventData[]): ICalendarEvent[] {
|
||||
return data.map((event): ICalendarEvent => ({
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end),
|
||||
type: event.type as CalendarEventType,
|
||||
allDay: event.allDay || false,
|
||||
syncStatus: 'synced' as const
|
||||
}));
|
||||
return data.map((event): ICalendarEvent => {
|
||||
// Validate event type constraints
|
||||
if (event.type === 'customer') {
|
||||
if (!event.bookingId) {
|
||||
console.warn(`Customer event ${event.id} missing bookingId`);
|
||||
}
|
||||
if (!event.resourceId) {
|
||||
console.warn(`Customer event ${event.id} missing resourceId`);
|
||||
}
|
||||
if (!event.customerId) {
|
||||
console.warn(`Customer event ${event.id} missing customerId`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end),
|
||||
type: event.type as CalendarEventType,
|
||||
allDay: event.allDay || false,
|
||||
|
||||
// Denormalized references (CRITICAL for booking architecture)
|
||||
bookingId: event.bookingId,
|
||||
resourceId: event.resourceId,
|
||||
customerId: event.customerId,
|
||||
|
||||
// Optional fields
|
||||
recurringId: event.recurringId,
|
||||
metadata: event.metadata,
|
||||
|
||||
syncStatus: 'synced' as const
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
src/repositories/MockResourceRepository.ts
Normal file
80
src/repositories/MockResourceRepository.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { IResource, ResourceType } from '../types/ResourceTypes';
|
||||
import { EntityType } from '../types/CalendarTypes';
|
||||
import { IApiRepository } from './IApiRepository';
|
||||
|
||||
interface RawResourceData {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
avatarUrl?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* MockResourceRepository - Loads resource data from local JSON file
|
||||
*
|
||||
* This repository implementation fetches mock resource data from a static JSON file.
|
||||
* Used for development and testing instead of API calls.
|
||||
*
|
||||
* Data Source: data/mock-resources.json
|
||||
*
|
||||
* NOTE: Create/Update/Delete operations are not supported - throws errors.
|
||||
* Only fetchAll() is implemented for loading initial mock data.
|
||||
*/
|
||||
export class MockResourceRepository implements IApiRepository<IResource> {
|
||||
public readonly entityType: EntityType = 'Resource';
|
||||
private readonly dataUrl = 'data/mock-resources.json';
|
||||
|
||||
/**
|
||||
* Fetch all resources from mock JSON file
|
||||
*/
|
||||
public async fetchAll(): Promise<IResource[]> {
|
||||
try {
|
||||
const response = await fetch(this.dataUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load mock resources: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const rawData: RawResourceData[] = await response.json();
|
||||
|
||||
return this.processResourceData(rawData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load resource data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockResourceRepository is read-only
|
||||
*/
|
||||
public async sendCreate(resource: IResource): Promise<IResource> {
|
||||
throw new Error('MockResourceRepository does not support sendCreate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockResourceRepository is read-only
|
||||
*/
|
||||
public async sendUpdate(id: string, updates: Partial<IResource>): Promise<IResource> {
|
||||
throw new Error('MockResourceRepository does not support sendUpdate. Mock data is read-only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT SUPPORTED - MockResourceRepository is read-only
|
||||
*/
|
||||
public async sendDelete(id: string): Promise<void> {
|
||||
throw new Error('MockResourceRepository does not support sendDelete. Mock data is read-only.');
|
||||
}
|
||||
|
||||
private processResourceData(data: RawResourceData[]): IResource[] {
|
||||
return data.map((resource): IResource => ({
|
||||
...resource,
|
||||
type: resource.type as ResourceType,
|
||||
syncStatus: 'synced' as const
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ 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.
|
||||
* to enable polymorphic operations across different entity types.
|
||||
*
|
||||
* 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.
|
||||
* POLYMORPHISM: Both SyncManager and DataSeeder work with Array<IEntityService<any>>
|
||||
* and use entityType property for runtime routing, avoiding switch statements.
|
||||
*/
|
||||
export interface IEntityService<T extends ISync> {
|
||||
/**
|
||||
|
|
@ -19,6 +19,30 @@ export interface IEntityService<T extends ISync> {
|
|||
*/
|
||||
readonly entityType: EntityType;
|
||||
|
||||
// ============================================================================
|
||||
// CRUD Operations (used by DataSeeder and other consumers)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all entities from IndexedDB
|
||||
* Used by DataSeeder to check if store is empty before seeding
|
||||
*
|
||||
* @returns Promise<T[]> - Array of all entities
|
||||
*/
|
||||
getAll(): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* Save an entity (create or update) to IndexedDB
|
||||
* Used by DataSeeder to persist fetched data
|
||||
*
|
||||
* @param entity - Entity to save
|
||||
*/
|
||||
save(entity: T): Promise<void>;
|
||||
|
||||
// ============================================================================
|
||||
// SYNC Methods (used by SyncManager)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Mark entity as successfully synced with backend
|
||||
* Sets syncStatus = 'synced' and persists to IndexedDB
|
||||
|
|
|
|||
103
src/workers/DataSeeder.ts
Normal file
103
src/workers/DataSeeder.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { IApiRepository } from '../repositories/IApiRepository';
|
||||
import { IEntityService } from '../storage/IEntityService';
|
||||
|
||||
/**
|
||||
* DataSeeder - Orchestrates initial data loading from repositories into IndexedDB
|
||||
*
|
||||
* ARCHITECTURE:
|
||||
* - Repository (Mock/Api): Fetches data from source (JSON file or backend API)
|
||||
* - DataSeeder (this class): Orchestrates fetch + save operations
|
||||
* - Service (EventService, etc.): Saves data to IndexedDB
|
||||
*
|
||||
* SEPARATION OF CONCERNS:
|
||||
* - Repository does NOT know about IndexedDB or storage
|
||||
* - Service does NOT know about where data comes from
|
||||
* - DataSeeder connects them together
|
||||
*
|
||||
* POLYMORPHIC DESIGN:
|
||||
* - Uses arrays of IEntityService<any>[] and IApiRepository<any>[]
|
||||
* - Matches services with repositories using entityType property
|
||||
* - Open/Closed Principle: Adding new entity requires no code changes here
|
||||
*
|
||||
* USAGE:
|
||||
* Called once during app initialization in index.ts:
|
||||
* 1. IndexedDBService.initialize() - open database
|
||||
* 2. dataSeeder.seedIfEmpty() - load initial data if needed
|
||||
* 3. CalendarManager.initialize() - start calendar
|
||||
*
|
||||
* NOTE: This is for INITIAL SEEDING only. Ongoing sync is handled by SyncManager.
|
||||
*/
|
||||
export class DataSeeder {
|
||||
constructor(
|
||||
// Arrays injected via DI - automatically includes all registered services/repositories
|
||||
private services: IEntityService<any>[],
|
||||
private repositories: IApiRepository<any>[]
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Seed all entity stores if they are empty
|
||||
* Runs on app initialization to load initial data from repositories
|
||||
*
|
||||
* Uses polymorphism: loops through all services and matches with repositories by entityType
|
||||
*/
|
||||
async seedIfEmpty(): Promise<void> {
|
||||
console.log('[DataSeeder] Checking if database needs seeding...');
|
||||
|
||||
try {
|
||||
// Loop through all entity services (Event, Booking, Customer, Resource, etc.)
|
||||
for (const service of this.services) {
|
||||
// Find matching repository for this service based on entityType
|
||||
const repository = this.repositories.find(repo => repo.entityType === service.entityType);
|
||||
|
||||
if (!repository) {
|
||||
console.warn(`[DataSeeder] No repository found for entity type: ${service.entityType}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Seed this entity type
|
||||
await this.seedEntity(service.entityType, service, repository);
|
||||
}
|
||||
|
||||
console.log('[DataSeeder] Seeding complete');
|
||||
} catch (error) {
|
||||
console.error('[DataSeeder] Seeding failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to seed a single entity type
|
||||
*
|
||||
* @param entityType - Entity type ('Event', 'Booking', 'Customer', 'Resource')
|
||||
* @param service - Entity service for IndexedDB operations
|
||||
* @param repository - Repository for fetching data
|
||||
*/
|
||||
private async seedEntity<T>(
|
||||
entityType: string,
|
||||
service: IEntityService<any>,
|
||||
repository: IApiRepository<T>
|
||||
): Promise<void> {
|
||||
// Check if store is empty
|
||||
const existing = await service.getAll();
|
||||
|
||||
if (existing.length > 0) {
|
||||
console.log(`[DataSeeder] ${entityType} store already has ${existing.length} items, skipping seed`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} store is empty, fetching from repository...`);
|
||||
|
||||
// Fetch from repository (Mock JSON or backend API)
|
||||
const data = await repository.fetchAll();
|
||||
|
||||
console.log(`[DataSeeder] Fetched ${data.length} ${entityType} items, saving to IndexedDB...`);
|
||||
|
||||
// Save each entity to IndexedDB
|
||||
// Note: Entities from repository should already have syncStatus='synced'
|
||||
for (const entity of data) {
|
||||
await service.save(entity);
|
||||
}
|
||||
|
||||
console.log(`[DataSeeder] ${entityType} seeding complete (${data.length} items saved)`);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue