This guide covers advanced patterns, best practices, and sophisticated use cases for azubiheft-api.
- 🏗️ Architecture Patterns
- ⚡ Performance Optimization
- 🔒 Security Best Practices
- 🤖 Automation Strategies
- 📊 Data Analytics & Insights
- 🔧 Extension & Customization
- 🌐 Integration Patterns
- 🧪 Testing Strategies
import { Session, SessionOptions } from 'azubiheft-api';
// Singleton pattern for application-wide session
class SessionManager {
private static instance: SessionManager;
private session: Session | null = null;
private lastActivity: Date = new Date();
private constructor() {}
static getInstance(): SessionManager {
if (!SessionManager.instance) {
SessionManager.instance = new SessionManager();
}
return SessionManager.instance;
}
async getSession(forceRefresh = false): Promise<Session> {
if (!this.session || forceRefresh || this.isSessionExpired()) {
await this.createNewSession();
}
this.lastActivity = new Date();
return this.session!;
}
private async createNewSession(): Promise<void> {
const options: SessionOptions = {
timeout: 60000,
userAgent: 'MyApp/2.0.0'
};
this.session = new Session(options);
await this.session.login({
username: process.env.AZUBIHEFT_USERNAME!,
password: process.env.AZUBIHEFT_PASSWORD!
});
}
private isSessionExpired(): boolean {
const TIMEOUT = 30 * 60 * 1000; // 30 minutes
return Date.now() - this.lastActivity.getTime() > TIMEOUT;
}
}import { Session, Entry, ReportEntry, Subject } from 'azubiheft-api';
interface ReportRepository {
findByDate(date: Date): Promise<ReportEntry[]>;
findByDateRange(start: Date, end: Date): Promise<ReportEntry[]>;
save(entries: Entry[]): Promise<void>;
delete(date: Date, entryNumber?: number): Promise<void>;
}
class AzubiheftReportRepository implements ReportRepository {
constructor(private session: Session) {}
async findByDate(date: Date): Promise<ReportEntry[]> {
return await this.session.getReport(date);
}
async findByDateRange(start: Date, end: Date): Promise<ReportEntry[]> {
const reports: ReportEntry[] = [];
const currentDate = new Date(start);
while (currentDate <= end) {
try {
const dayReports = await this.session.getReport(currentDate);
reports.push(...dayReports);
} catch (error) {
console.warn(`No reports for ${currentDate.toDateString()}`);
}
currentDate.setDate(currentDate.getDate() + 1);
}
return reports;
}
async save(entries: Entry[]): Promise<void> {
await this.session.writeReports(entries);
}
async delete(date: Date, entryNumber?: number): Promise<void> {
await this.session.deleteReport(date, entryNumber);
}
}interface Command {
execute(): Promise<void>;
undo(): Promise<void>;
canUndo(): boolean;
}
class AddReportCommand implements Command {
private executed = false;
constructor(
private repository: ReportRepository,
private entry: Entry
) {}
async execute(): Promise<void> {
await this.repository.save([this.entry]);
this.executed = true;
}
async undo(): Promise<void> {
if (this.executed) {
await this.repository.delete(this.entry.date);
this.executed = false;
}
}
canUndo(): boolean {
return this.executed;
}
}
class CommandManager {
private history: Command[] = [];
private currentIndex = -1;
async execute(command: Command): Promise<void> {
await command.execute();
// Remove any commands after current index
this.history.splice(this.currentIndex + 1);
this.history.push(command);
this.currentIndex++;
}
async undo(): Promise<void> {
if (this.currentIndex >= 0) {
const command = this.history[this.currentIndex];
if (command.canUndo()) {
await command.undo();
this.currentIndex--;
}
}
}
}class PerformantSessionManager {
private sessionPool: Session[] = [];
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async getOptimalSession(): Promise<Session> {
// Return available session from pool or create new one
if (this.sessionPool.length > 0) {
return this.sessionPool.pop()!;
}
return await this.createNewSession();
}
async releaseSession(session: Session): Promise<void> {
// Return session to pool for reuse
this.sessionPool.push(session);
}
async getCachedData<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.data as T;
}
const data = await fetcher();
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
async getSubjectsOptimized(): Promise<Subject[]> {
return this.getCachedData('subjects', async () => {
const session = await this.getOptimalSession();
try {
return await session.getSubjects();
} finally {
await this.releaseSession(session);
}
});
}
}interface QueueItem {
id: string;
operation: () => Promise<any>;
priority: number;
retries: number;
}
class OperationQueue {
private queue: QueueItem[] = [];
private processing = false;
private concurrency = 3;
private activeOperations = 0;
async add(operation: () => Promise<any>, priority = 0): Promise<string> {
const id = `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.queue.push({
id,
operation,
priority,
retries: 0
});
this.queue.sort((a, b) => b.priority - a.priority);
this.processQueue();
return id;
}
private async processQueue(): Promise<void> {
if (this.processing || this.activeOperations >= this.concurrency) {
return;
}
this.processing = true;
while (this.queue.length > 0 && this.activeOperations < this.concurrency) {
const item = this.queue.shift()!;
this.executeOperation(item);
}
this.processing = false;
}
private async executeOperation(item: QueueItem): Promise<void> {
this.activeOperations++;
try {
await item.operation();
} catch (error) {
if (item.retries < 3) {
item.retries++;
this.queue.unshift(item); // Retry at front of queue
} else {
console.error(`Operation ${item.id} failed after 3 retries:`, error);
}
} finally {
this.activeOperations--;
this.processQueue();
}
}
}async function* processLargeDataset(
repository: ReportRepository,
startDate: Date,
endDate: Date,
batchSize = 7 // Process week by week
): AsyncGenerator<ReportEntry[], void, unknown> {
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
const batchEnd = new Date(currentDate);
batchEnd.setDate(currentDate.getDate() + batchSize - 1);
if (batchEnd > endDate) {
batchEnd.setTime(endDate.getTime());
}
const batch = await repository.findByDateRange(currentDate, batchEnd);
if (batch.length > 0) {
yield batch;
}
currentDate.setDate(batchEnd.getDate() + 1);
}
}
// Usage example
async function analyzeYearOfData(repository: ReportRepository) {
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-12-31');
for await (const batch of processLargeDataset(repository, startDate, endDate)) {
// Process each batch without loading entire year into memory
console.log(`Processing batch of ${batch.length} entries`);
// Your processing logic here
await processBatch(batch);
}
}interface SecureCredentials {
username: string;
password: string;
mfaToken?: string;
}
class CredentialManager {
private static instance: CredentialManager;
private credentials: SecureCredentials | null = null;
private constructor() {}
static getInstance(): CredentialManager {
if (!CredentialManager.instance) {
CredentialManager.instance = new CredentialManager();
}
return CredentialManager.instance;
}
async loadCredentials(): Promise<SecureCredentials> {
if (this.credentials) {
return this.credentials;
}
// Load from secure sources
this.credentials = {
username: this.getFromSecureSource('AZUBIHEFT_USERNAME'),
password: this.getFromSecureSource('AZUBIHEFT_PASSWORD'),
mfaToken: this.getFromSecureSource('AZUBIHEFT_MFA_TOKEN', false)
};
return this.credentials;
}
private getFromSecureSource(key: string, required = true): string {
// Try environment variables first
let value = process.env[key];
if (!value && typeof window !== 'undefined') {
// Browser environment - get from secure storage
value = this.getFromSecureStorage(key);
}
if (!value && required) {
throw new Error(`Required credential ${key} not found`);
}
return value || '';
}
private getFromSecureStorage(key: string): string | null {
// Implementation would depend on your secure storage solution
// Could be encrypted localStorage, secure vault, etc.
return localStorage.getItem(`secure_${key}`);
}
clearCredentials(): void {
this.credentials = null;
// Clear from all storage locations
if (typeof window !== 'undefined') {
Object.keys(localStorage).forEach(key => {
if (key.startsWith('secure_')) {
localStorage.removeItem(key);
}
});
}
}
}class InputValidator {
static validateReportMessage(message: string): string {
if (!message || typeof message !== 'string') {
throw new Error('Message must be a non-empty string');
}
// Remove potentially dangerous characters
const sanitized = message
.replace(/[<>]/g, '') // Remove angle brackets
.replace(/javascript:/gi, '') // Remove javascript: protocol
.replace(/on\w+=/gi, '') // Remove event handlers
.trim();
if (sanitized.length === 0) {
throw new Error('Message cannot be empty after sanitization');
}
if (sanitized.length > 5000) {
throw new Error('Message too long (max 5000 characters)');
}
return sanitized;
}
static validateTimeString(timeString: string): string {
if (!timeString || typeof timeString !== 'string') {
throw new Error('Time string must be provided');
}
const timePattern = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
if (!timePattern.test(timeString)) {
throw new Error('Invalid time format. Expected HH:MM');
}
return timeString;
}
static validateDate(date: any): Date {
let validDate: Date;
if (date instanceof Date) {
validDate = date;
} else if (typeof date === 'string') {
validDate = new Date(date);
} else {
throw new Error('Date must be Date object or ISO string');
}
if (isNaN(validDate.getTime())) {
throw new Error('Invalid date provided');
}
// Check if date is in reasonable range
const now = new Date();
const oneYearAgo = new Date(now.getFullYear() - 1, 0, 1);
const oneYearFromNow = new Date(now.getFullYear() + 1, 11, 31);
if (validDate < oneYearAgo || validDate > oneYearFromNow) {
throw new Error('Date must be within reasonable range (last year to next year)');
}
return validDate;
}
}interface AuditLogEntry {
timestamp: Date;
userId: string;
action: string;
resource: string;
details: Record<string, any>;
ipAddress?: string;
userAgent?: string;
}
class AuditLogger {
private logs: AuditLogEntry[] = [];
async logAction(
userId: string,
action: string,
resource: string,
details: Record<string, any> = {},
context?: { ipAddress?: string; userAgent?: string }
): Promise<void> {
const entry: AuditLogEntry = {
timestamp: new Date(),
userId,
action,
resource,
details: this.sanitizeDetails(details),
ipAddress: context?.ipAddress,
userAgent: context?.userAgent
};
this.logs.push(entry);
// In production, send to logging service
await this.persistLog(entry);
}
private sanitizeDetails(details: Record<string, any>): Record<string, any> {
const sanitized = { ...details };
// Remove sensitive information
const sensitiveKeys = ['password', 'token', 'secret', 'key'];
sensitiveKeys.forEach(key => {
if (key in sanitized) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
private async persistLog(entry: AuditLogEntry): Promise<void> {
// Implementation would send to logging service
console.log('AUDIT:', JSON.stringify(entry));
}
getRecentLogs(limit = 100): AuditLogEntry[] {
return this.logs.slice(-limit);
}
}interface AutomationEvent {
type: string;
payload: any;
timestamp: Date;
}
class AutomationEngine {
private eventHandlers = new Map<string, Array<(event: AutomationEvent) => Promise<void>>>();
private scheduledTasks = new Map<string, NodeJS.Timeout>();
on(eventType: string, handler: (event: AutomationEvent) => Promise<void>): void {
if (!this.eventHandlers.has(eventType)) {
this.eventHandlers.set(eventType, []);
}
this.eventHandlers.get(eventType)!.push(handler);
}
async emit(eventType: string, payload: any): Promise<void> {
const event: AutomationEvent = {
type: eventType,
payload,
timestamp: new Date()
};
const handlers = this.eventHandlers.get(eventType) || [];
await Promise.all(handlers.map(handler => handler(event)));
}
scheduleDaily(time: string, task: () => Promise<void>): string {
const taskId = `daily_${Date.now()}`;
const scheduleNext = () => {
const now = new Date();
const [hours, minutes] = time.split(':').map(Number);
const scheduledTime = new Date(now);
scheduledTime.setHours(hours, minutes, 0, 0);
// If time has passed today, schedule for tomorrow
if (scheduledTime <= now) {
scheduledTime.setDate(scheduledTime.getDate() + 1);
}
const delay = scheduledTime.getTime() - now.getTime();
this.scheduledTasks.set(taskId, setTimeout(async () => {
try {
await task();
} catch (error) {
console.error(`Scheduled task ${taskId} failed:`, error);
}
scheduleNext(); // Schedule next execution
}, delay));
};
scheduleNext();
return taskId;
}
cancelScheduledTask(taskId: string): void {
const timeout = this.scheduledTasks.get(taskId);
if (timeout) {
clearTimeout(timeout);
this.scheduledTasks.delete(taskId);
}
}
}
// Usage example
const automation = new AutomationEngine();
// Set up event handlers
automation.on('report_added', async (event) => {
console.log('Report added:', event.payload);
// Trigger additional processing
});
automation.on('week_completed', async (event) => {
// Generate weekly summary
console.log('Week completed, generating summary...');
});
// Schedule daily tasks
automation.scheduleDaily('18:00', async () => {
// Daily summary task
console.log('Generating daily summary...');
});interface Template {
id: string;
name: string;
pattern: string;
variables: Record<string, any>;
conditions?: Array<(context: any) => boolean>;
}
class SmartTemplateEngine {
private templates: Map<string, Template> = new Map();
addTemplate(template: Template): void {
this.templates.set(template.id, template);
}
async generateContent(templateId: string, context: Record<string, any>): Promise<string> {
const template = this.templates.get(templateId);
if (!template) {
throw new Error(`Template ${templateId} not found`);
}
// Check conditions
if (template.conditions) {
const conditionsMet = template.conditions.every(condition => condition(context));
if (!conditionsMet) {
throw new Error(`Template conditions not met for ${templateId}`);
}
}
// Replace variables in pattern
let content = template.pattern;
// Built-in variables
const builtInVars = {
date: new Date().toLocaleDateString(),
time: new Date().toLocaleTimeString(),
dayOfWeek: new Date().toLocaleDateString('en', { weekday: 'long' }),
...template.variables,
...context
};
// Replace placeholders
for (const [key, value] of Object.entries(builtInVars)) {
const placeholder = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
content = content.replace(placeholder, String(value));
}
// Advanced transformations
content = await this.applyTransformations(content, context);
return content;
}
private async applyTransformations(content: string, context: any): Promise<string> {
// Apply conditional blocks
content = content.replace(
/\{\{#if\s+(\w+)\}\}(.*?)\{\{\/if\}\}/gs,
(match, condition, block) => {
return context[condition] ? block : '';
}
);
// Apply loops
content = content.replace(
/\{\{#each\s+(\w+)\}\}(.*?)\{\{\/each\}\}/gs,
(match, arrayName, block) => {
const array = context[arrayName] || [];
return array.map((item: any, index: number) => {
let itemBlock = block;
itemBlock = itemBlock.replace(/\{\{@index\}\}/g, index.toString());
itemBlock = itemBlock.replace(/\{\{this\}\}/g, item.toString());
return itemBlock;
}).join('');
}
);
return content;
}
}
// Usage example
const templateEngine = new SmartTemplateEngine();
templateEngine.addTemplate({
id: 'daily_work',
name: 'Daily Work Report',
pattern: `Worked on {{project}} for {{duration}}. {{#if achievements}}Key achievements: {{#each achievements}}{{this}}{{/each}}{{/if}}`,
variables: {
project: 'Current Project'
},
conditions: [
(context) => context.duration && context.duration !== '00:00'
]
});interface MetricDefinition {
name: string;
calculate: (data: ReportEntry[]) => number | string;
format?: (value: any) => string;
}
class AnalyticsEngine {
private metrics: Map<string, MetricDefinition> = new Map();
constructor() {
this.registerBuiltInMetrics();
}
private registerBuiltInMetrics(): void {
this.addMetric({
name: 'total_hours',
calculate: (data) => {
const totalMinutes = data.reduce((sum, entry) => {
return sum + TimeHelper.timeStringToMinutes(entry.duration);
}, 0);
return TimeHelper.minutesToTimeString(totalMinutes);
}
});
this.addMetric({
name: 'productivity_score',
calculate: (data) => {
// Complex productivity calculation
const weights = { 'Betrieb': 1.0, 'Schule': 0.8, 'ÜBA': 0.9 };
const weightedMinutes = data.reduce((sum, entry) => {
const minutes = TimeHelper.timeStringToMinutes(entry.duration);
const weight = weights[entry.type as keyof typeof weights] || 0.5;
return sum + (minutes * weight);
}, 0);
return Math.round((weightedMinutes / (8 * 60)) * 100); // Percentage of 8-hour day
},
format: (value) => `${value}%`
});
this.addMetric({
name: 'learning_velocity',
calculate: (data) => {
const learningEntries = data.filter(entry =>
entry.text.toLowerCase().includes('learn') ||
entry.type === 'Schule'
);
return learningEntries.length;
},
format: (value) => `${value} learning activities`
});
}
addMetric(metric: MetricDefinition): void {
this.metrics.set(metric.name, metric);
}
async generateInsights(data: ReportEntry[]): Promise<Record<string, any>> {
const insights: Record<string, any> = {};
for (const [name, metric] of this.metrics) {
try {
const value = metric.calculate(data);
insights[name] = {
value,
formatted: metric.format ? metric.format(value) : value
};
} catch (error) {
console.error(`Error calculating metric ${name}:`, error);
insights[name] = { value: null, error: error.message };
}
}
return insights;
}
async generateTrendAnalysis(
historicalData: Array<{ period: string; data: ReportEntry[] }>
): Promise<Record<string, any>> {
const trends: Record<string, any> = {};
for (const [metricName] of this.metrics) {
const values = historicalData.map(period => {
const insights = this.generateInsights(period.data);
return {
period: period.period,
value: insights[metricName]?.value || 0
};
});
trends[metricName] = {
values,
trend: this.calculateTrend(values.map(v => v.value)),
summary: this.generateTrendSummary(values)
};
}
return trends;
}
private calculateTrend(values: number[]): 'increasing' | 'decreasing' | 'stable' {
if (values.length < 2) return 'stable';
const slope = this.calculateLinearRegression(values).slope;
if (Math.abs(slope) < 0.1) return 'stable';
return slope > 0 ? 'increasing' : 'decreasing';
}
private calculateLinearRegression(values: number[]): { slope: number; intercept: number } {
const n = values.length;
const sumX = values.reduce((sum, _, i) => sum + i, 0);
const sumY = values.reduce((sum, val) => sum + val, 0);
const sumXY = values.reduce((sum, val, i) => sum + (i * val), 0);
const sumXX = values.reduce((sum, _, i) => sum + (i * i), 0);
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
return { slope, intercept };
}
private generateTrendSummary(values: Array<{ period: string; value: number }>): string {
const latest = values[values.length - 1]?.value || 0;
const previous = values[values.length - 2]?.value || 0;
const change = latest - previous;
const changePercent = previous !== 0 ? Math.round((change / previous) * 100) : 0;
if (Math.abs(changePercent) < 5) {
return 'Stable performance';
}
return changePercent > 0
? `Improved by ${changePercent}% from last period`
: `Decreased by ${Math.abs(changePercent)}% from last period`;
}
}class PredictiveAnalytics {
async predictNextWeekHours(historicalData: ReportEntry[]): Promise<{
predicted: string;
confidence: number;
factors: string[];
}> {
// Group data by week
const weeklyData = this.groupByWeek(historicalData);
if (weeklyData.length < 4) {
return {
predicted: '40:00',
confidence: 0.3,
factors: ['Insufficient historical data']
};
}
// Calculate weekly totals
const weeklyHours = weeklyData.map(week => {
const totalMinutes = week.entries.reduce((sum, entry) => {
return sum + TimeHelper.timeStringToMinutes(entry.duration);
}, 0);
return totalMinutes;
});
// Simple moving average with trend adjustment
const recentWeeks = weeklyHours.slice(-4);
const average = recentWeeks.reduce((sum, val) => sum + val, 0) / recentWeeks.length;
// Calculate trend
const trend = this.calculateTrend(recentWeeks);
const trendAdjustment = trend * 0.1; // 10% trend influence
const predicted = Math.round(average + trendAdjustment);
const confidence = this.calculateConfidence(weeklyHours);
return {
predicted: TimeHelper.minutesToTimeString(predicted),
confidence,
factors: this.identifyInfluencingFactors(historicalData)
};
}
private groupByWeek(data: ReportEntry[]): Array<{ week: number; year: number; entries: ReportEntry[] }> {
const weeks = new Map<string, ReportEntry[]>();
data.forEach(entry => {
// Simplified week grouping - in real implementation, use actual dates
const weekKey = '2024-W01'; // Placeholder
if (!weeks.has(weekKey)) {
weeks.set(weekKey, []);
}
weeks.get(weekKey)!.push(entry);
});
return Array.from(weeks.entries()).map(([key, entries]) => ({
week: 1, // Placeholder
year: 2024, // Placeholder
entries
}));
}
private calculateTrend(values: number[]): number {
// Linear regression slope
const n = values.length;
const sumX = values.reduce((sum, _, i) => sum + i, 0);
const sumY = values.reduce((sum, val) => sum + val, 0);
const sumXY = values.reduce((sum, val, i) => sum + (i * val), 0);
const sumXX = values.reduce((sum, _, i) => sum + (i * i), 0);
return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
}
private calculateConfidence(values: number[]): number {
if (values.length < 3) return 0.3;
// Calculate coefficient of variation
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
const standardDeviation = Math.sqrt(variance);
const coefficientOfVariation = standardDeviation / mean;
// Higher consistency = higher confidence
return Math.max(0.1, Math.min(0.9, 1 - coefficientOfVariation));
}
private identifyInfluencingFactors(data: ReportEntry[]): string[] {
const factors: string[] = [];
// Analyze patterns
const typeDistribution = this.analyzeTypeDistribution(data);
const timePatterns = this.analyzeTimePatterns(data);
if (typeDistribution.workHeavy) {
factors.push('Work-heavy schedule detected');
}
if (timePatterns.overtime) {
factors.push('Recent overtime trend');
}
if (timePatterns.consistent) {
factors.push('Consistent daily patterns');
}
return factors;
}
private analyzeTypeDistribution(data: ReportEntry[]): { workHeavy: boolean } {
const workEntries = data.filter(e => e.type === 'Betrieb').length;
const totalEntries = data.length;
return {
workHeavy: workEntries / totalEntries > 0.7
};
}
private analyzeTimePatterns(data: ReportEntry[]): { overtime: boolean; consistent: boolean } {
// Simplified analysis
const dailyTotals = new Map<string, number>();
data.forEach(entry => {
const day = '2024-01-01'; // Placeholder - would use actual date
const minutes = TimeHelper.timeStringToMinutes(entry.duration);
dailyTotals.set(day, (dailyTotals.get(day) || 0) + minutes);
});
const totals = Array.from(dailyTotals.values());
const averageDaily = totals.reduce((sum, val) => sum + val, 0) / totals.length;
const hasOvertime = totals.some(total => total > 8 * 60); // > 8 hours
// Calculate consistency (low variance = high consistency)
const variance = totals.reduce((sum, val) => sum + Math.pow(val - averageDaily, 2), 0) / totals.length;
const consistent = variance < (2 * 60 * 2 * 60); // Less than 2 hours variance
return {
overtime: hasOvertime,
consistent
};
}
}This advanced usage guide demonstrates sophisticated patterns for building production-ready applications with azubiheft-api. The patterns shown here can be adapted and extended based on your specific requirements.
Continue reading the other documentation files for troubleshooting, migration guides, and complete API reference.