You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
550 lines
14 KiB
550 lines
14 KiB
/**
|
|
* Configuration loader for TimeSafari test apps
|
|
*
|
|
* Loads configuration from JSON files and provides typed access
|
|
* to TimeSafari-specific settings, Endorser.ch API endpoints,
|
|
* and test data.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
export interface TimeSafariConfig {
|
|
timesafari: {
|
|
appId: string;
|
|
appName: string;
|
|
version: string;
|
|
description: string;
|
|
};
|
|
endorser: {
|
|
baseUrl: string;
|
|
apiVersion: string;
|
|
endpoints: {
|
|
offers: string;
|
|
offersToPlans: string;
|
|
plansLastUpdated: string;
|
|
notificationsBundle: string;
|
|
};
|
|
authentication: {
|
|
type: string;
|
|
token: string;
|
|
headers: Record<string, string>;
|
|
};
|
|
pagination: {
|
|
defaultLimit: number;
|
|
maxLimit: number;
|
|
hitLimitThreshold: number;
|
|
};
|
|
};
|
|
notificationTypes: {
|
|
offers: {
|
|
enabled: boolean;
|
|
types: string[];
|
|
};
|
|
projects: {
|
|
enabled: boolean;
|
|
types: string[];
|
|
};
|
|
people: {
|
|
enabled: boolean;
|
|
types: string[];
|
|
};
|
|
items: {
|
|
enabled: boolean;
|
|
types: string[];
|
|
};
|
|
};
|
|
scheduling: {
|
|
contentFetch: {
|
|
schedule: string;
|
|
time: string;
|
|
description: string;
|
|
};
|
|
userNotification: {
|
|
schedule: string;
|
|
time: string;
|
|
description: string;
|
|
};
|
|
};
|
|
testData: {
|
|
userDid: string;
|
|
starredPlanIds: string[];
|
|
lastKnownOfferId: string;
|
|
lastKnownPlanId: string;
|
|
mockOffers: any[];
|
|
mockProjects: any[];
|
|
};
|
|
callbacks: {
|
|
offers: {
|
|
enabled: boolean;
|
|
localHandler: string;
|
|
};
|
|
projects: {
|
|
enabled: boolean;
|
|
localHandler: string;
|
|
};
|
|
people: {
|
|
enabled: boolean;
|
|
localHandler: string;
|
|
};
|
|
items: {
|
|
enabled: boolean;
|
|
localHandler: string;
|
|
};
|
|
communityAnalytics: {
|
|
enabled: boolean;
|
|
endpoint: string;
|
|
headers: Record<string, string>;
|
|
};
|
|
};
|
|
observability: {
|
|
enableLogging: boolean;
|
|
logLevel: string;
|
|
enableMetrics: boolean;
|
|
enableHealthChecks: boolean;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Configuration loader class
|
|
*/
|
|
export class ConfigLoader {
|
|
private static instance: ConfigLoader;
|
|
private config: TimeSafariConfig | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static getInstance(): ConfigLoader {
|
|
if (!ConfigLoader.instance) {
|
|
ConfigLoader.instance = new ConfigLoader();
|
|
}
|
|
return ConfigLoader.instance;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from JSON file
|
|
*/
|
|
public async loadConfig(): Promise<TimeSafariConfig> {
|
|
if (this.config) {
|
|
return this.config;
|
|
}
|
|
|
|
try {
|
|
// In a real app, this would fetch from a config file
|
|
// For test apps, we'll use a hardcoded config
|
|
this.config = {
|
|
timesafari: {
|
|
appId: "app.timesafari.test",
|
|
appName: "TimeSafari Test",
|
|
version: "1.0.0",
|
|
description: "Test app for TimeSafari Daily Notification Plugin integration"
|
|
},
|
|
endorser: {
|
|
baseUrl: "http://localhost:3001",
|
|
apiVersion: "v2",
|
|
endpoints: {
|
|
offers: "/api/v2/report/offers",
|
|
offersToPlans: "/api/v2/report/offersToPlansOwnedByMe",
|
|
plansLastUpdated: "/api/v2/report/plansLastUpdatedBetween",
|
|
notificationsBundle: "/api/v2/report/notifications/bundle"
|
|
},
|
|
authentication: {
|
|
type: "Bearer",
|
|
token: "test-jwt-token-12345",
|
|
headers: {
|
|
"Authorization": "Bearer test-jwt-token-12345",
|
|
"Content-Type": "application/json",
|
|
"X-Privacy-Level": "user-controlled"
|
|
}
|
|
},
|
|
pagination: {
|
|
defaultLimit: 50,
|
|
maxLimit: 100,
|
|
hitLimitThreshold: 50
|
|
}
|
|
},
|
|
notificationTypes: {
|
|
offers: {
|
|
enabled: true,
|
|
types: [
|
|
"new_to_me",
|
|
"changed_to_me",
|
|
"new_to_projects",
|
|
"changed_to_projects",
|
|
"new_to_favorites",
|
|
"changed_to_favorites"
|
|
]
|
|
},
|
|
projects: {
|
|
enabled: true,
|
|
types: [
|
|
"local_new",
|
|
"local_changed",
|
|
"content_interest_new",
|
|
"favorited_changed"
|
|
]
|
|
},
|
|
people: {
|
|
enabled: true,
|
|
types: [
|
|
"local_new",
|
|
"local_changed",
|
|
"content_interest_new",
|
|
"favorited_changed",
|
|
"contacts_changed"
|
|
]
|
|
},
|
|
items: {
|
|
enabled: true,
|
|
types: [
|
|
"local_new",
|
|
"local_changed",
|
|
"favorited_changed"
|
|
]
|
|
}
|
|
},
|
|
scheduling: {
|
|
contentFetch: {
|
|
schedule: "0 8 * * *",
|
|
time: "08:00",
|
|
description: "8 AM daily - fetch community updates"
|
|
},
|
|
userNotification: {
|
|
schedule: "0 9 * * *",
|
|
time: "09:00",
|
|
description: "9 AM daily - notify users of community updates"
|
|
}
|
|
},
|
|
testData: {
|
|
userDid: "did:example:testuser123",
|
|
starredPlanIds: [
|
|
"plan-community-garden",
|
|
"plan-local-food",
|
|
"plan-sustainability"
|
|
],
|
|
lastKnownOfferId: "01HSE3R9MAC0FT3P3KZ382TWV7",
|
|
lastKnownPlanId: "01HSE3R9MAC0FT3P3KZ382TWV8",
|
|
mockOffers: [
|
|
{
|
|
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV7",
|
|
handleId: "offer-web-dev-001",
|
|
offeredByDid: "did:example:offerer123",
|
|
recipientDid: "did:example:testuser123",
|
|
objectDescription: "Web development services for community project",
|
|
unit: "USD",
|
|
amount: 1000,
|
|
amountGiven: 500,
|
|
amountGivenConfirmed: 250
|
|
}
|
|
],
|
|
mockProjects: [
|
|
{
|
|
plan: {
|
|
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV8",
|
|
handleId: "plan-community-garden",
|
|
name: "Community Garden Project",
|
|
description: "Building a community garden for local food production",
|
|
issuerDid: "did:example:issuer123",
|
|
agentDid: "did:example:agent123"
|
|
},
|
|
wrappedClaimBefore: null
|
|
}
|
|
]
|
|
},
|
|
callbacks: {
|
|
offers: {
|
|
enabled: true,
|
|
localHandler: "handleOffersNotification"
|
|
},
|
|
projects: {
|
|
enabled: true,
|
|
localHandler: "handleProjectsNotification"
|
|
},
|
|
people: {
|
|
enabled: true,
|
|
localHandler: "handlePeopleNotification"
|
|
},
|
|
items: {
|
|
enabled: true,
|
|
localHandler: "handleItemsNotification"
|
|
},
|
|
communityAnalytics: {
|
|
enabled: true,
|
|
endpoint: "http://localhost:3001/api/analytics/community-events",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-Privacy-Level": "aggregated"
|
|
}
|
|
}
|
|
},
|
|
observability: {
|
|
enableLogging: true,
|
|
logLevel: "debug",
|
|
enableMetrics: true,
|
|
enableHealthChecks: true
|
|
}
|
|
};
|
|
|
|
return this.config;
|
|
} catch (error) {
|
|
console.error('Failed to load configuration:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get configuration
|
|
*/
|
|
public getConfig(): TimeSafariConfig {
|
|
if (!this.config) {
|
|
throw new Error('Configuration not loaded. Call loadConfig() first.');
|
|
}
|
|
return this.config;
|
|
}
|
|
|
|
/**
|
|
* Get Endorser.ch API URL for a specific endpoint
|
|
*/
|
|
public getEndorserUrl(endpoint: keyof TimeSafariConfig['endorser']['endpoints']): string {
|
|
const config = this.getConfig();
|
|
return `${config.endorser.baseUrl}${config.endorser.endpoints[endpoint]}`;
|
|
}
|
|
|
|
/**
|
|
* Get authentication headers
|
|
*/
|
|
public getAuthHeaders(): Record<string, string> {
|
|
const config = this.getConfig();
|
|
return config.endorser.authentication.headers;
|
|
}
|
|
|
|
/**
|
|
* Get test data
|
|
*/
|
|
public getTestData() {
|
|
const config = this.getConfig();
|
|
return config.testData;
|
|
}
|
|
|
|
/**
|
|
* Get notification types for a specific category
|
|
*/
|
|
public getNotificationTypes(category: keyof TimeSafariConfig['notificationTypes']) {
|
|
const config = this.getConfig();
|
|
return config.notificationTypes[category];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logger utility for test apps
|
|
*/
|
|
export class TestLogger {
|
|
private logLevel: string;
|
|
private logs: string[] = [];
|
|
|
|
constructor(logLevel: string = 'debug') {
|
|
this.logLevel = logLevel;
|
|
}
|
|
|
|
private shouldLog(level: string): boolean {
|
|
const levels = ['error', 'warn', 'info', 'debug'];
|
|
return levels.indexOf(level) <= levels.indexOf(this.logLevel);
|
|
}
|
|
|
|
private addToLogs(level: string, message: string, data?: any): void {
|
|
const timestamp = new Date().toISOString();
|
|
const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
this.logs.push(logEntry);
|
|
|
|
// Keep only last 1000 logs to prevent memory issues
|
|
if (this.logs.length > 1000) {
|
|
this.logs = this.logs.slice(-1000);
|
|
}
|
|
}
|
|
|
|
public debug(message: string, data?: any) {
|
|
if (this.shouldLog('debug')) {
|
|
console.log(`[DEBUG] ${message}`, data || '');
|
|
this.addToLogs('debug', message, data);
|
|
}
|
|
}
|
|
|
|
public info(message: string, data?: any) {
|
|
if (this.shouldLog('info')) {
|
|
console.log(`[INFO] ${message}`, data || '');
|
|
this.addToLogs('info', message, data);
|
|
}
|
|
}
|
|
|
|
public warn(message: string, data?: any) {
|
|
if (this.shouldLog('warn')) {
|
|
console.warn(`[WARN] ${message}`, data || '');
|
|
this.addToLogs('warn', message, data);
|
|
}
|
|
}
|
|
|
|
public error(message: string, data?: any) {
|
|
if (this.shouldLog('error')) {
|
|
console.error(`[ERROR] ${message}`, data || '');
|
|
this.addToLogs('error', message, data);
|
|
}
|
|
}
|
|
|
|
public getLogs(): string[] {
|
|
return [...this.logs];
|
|
}
|
|
|
|
public clearLogs(): void {
|
|
this.logs = [];
|
|
}
|
|
|
|
public exportLogs(): string {
|
|
return this.logs.join('\n');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock DailyNotificationService for test apps
|
|
*/
|
|
export class MockDailyNotificationService {
|
|
private config: TimeSafariConfig;
|
|
private logger: TestLogger;
|
|
private isInitialized = false;
|
|
|
|
constructor(config: TimeSafariConfig) {
|
|
this.config = config;
|
|
this.logger = new TestLogger(config.observability.logLevel);
|
|
}
|
|
|
|
/**
|
|
* Initialize the service
|
|
*/
|
|
public async initialize(): Promise<void> {
|
|
this.logger.info('Initializing Mock DailyNotificationService');
|
|
this.isInitialized = true;
|
|
this.logger.info('Mock DailyNotificationService initialized successfully');
|
|
}
|
|
|
|
/**
|
|
* Schedule dual notification (content fetch + user notification)
|
|
*/
|
|
public async scheduleDualNotification(config: any): Promise<void> {
|
|
if (!this.isInitialized) {
|
|
throw new Error('Service not initialized');
|
|
}
|
|
|
|
this.logger.info('Scheduling dual notification', config);
|
|
|
|
// Simulate content fetch
|
|
if (config.contentFetch?.enabled) {
|
|
await this.simulateContentFetch(config.contentFetch);
|
|
}
|
|
|
|
// Simulate user notification
|
|
if (config.userNotification?.enabled) {
|
|
await this.simulateUserNotification(config.userNotification);
|
|
}
|
|
|
|
this.logger.info('Dual notification scheduled successfully');
|
|
}
|
|
|
|
/**
|
|
* Register callback
|
|
*/
|
|
public async registerCallback(name: string, callback: Function): Promise<void> {
|
|
this.logger.info(`Registering callback: ${name}`);
|
|
// In a real implementation, this would register the callback
|
|
this.logger.info(`Callback ${name} registered successfully`);
|
|
}
|
|
|
|
/**
|
|
* Get dual schedule status
|
|
*/
|
|
public async getDualScheduleStatus(): Promise<any> {
|
|
return {
|
|
contentFetch: {
|
|
enabled: true,
|
|
lastFetch: new Date().toISOString(),
|
|
nextFetch: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
|
},
|
|
userNotification: {
|
|
enabled: true,
|
|
lastNotification: new Date().toISOString(),
|
|
nextNotification: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cancel all notifications
|
|
*/
|
|
public async cancelAllNotifications(): Promise<void> {
|
|
this.logger.info('Cancelling all notifications');
|
|
this.logger.info('All notifications cancelled successfully');
|
|
}
|
|
|
|
/**
|
|
* Simulate content fetch using Endorser.ch API patterns
|
|
*/
|
|
private async simulateContentFetch(config: any): Promise<void> {
|
|
this.logger.info('Simulating content fetch from Endorser.ch API');
|
|
|
|
try {
|
|
// Simulate parallel API requests
|
|
const testData = this.config.testData;
|
|
|
|
// Mock offers to person
|
|
const offersToPerson = {
|
|
data: testData.mockOffers,
|
|
hitLimit: false
|
|
};
|
|
|
|
// Mock offers to projects
|
|
const offersToProjects = {
|
|
data: [],
|
|
hitLimit: false
|
|
};
|
|
|
|
// Mock starred project changes
|
|
const starredChanges = {
|
|
data: testData.mockProjects,
|
|
hitLimit: false
|
|
};
|
|
|
|
this.logger.info('Content fetch simulation completed', {
|
|
offersToPerson: offersToPerson.data.length,
|
|
offersToProjects: offersToProjects.data.length,
|
|
starredChanges: starredChanges.data.length
|
|
});
|
|
|
|
// Call success callback if provided
|
|
if (config.callbacks?.onSuccess) {
|
|
await config.callbacks.onSuccess({
|
|
offersToPerson,
|
|
offersToProjects,
|
|
starredChanges
|
|
});
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('Content fetch simulation failed', error);
|
|
if (config.callbacks?.onError) {
|
|
await config.callbacks.onError(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simulate user notification
|
|
*/
|
|
private async simulateUserNotification(config: any): Promise<void> {
|
|
this.logger.info('Simulating user notification', {
|
|
title: config.title,
|
|
body: config.body,
|
|
time: config.schedule
|
|
});
|
|
this.logger.info('User notification simulation completed');
|
|
}
|
|
}
|
|
|