@bernierllc/content-editor-service
v0.1.5
Published
Editor orchestration and coordination service for content management
Readme
@bernierllc/content-editor-service
Editor orchestration and coordination service for content management.
Overview
This package provides a service layer for orchestrating content editing operations, including auto-save management, soft delete functionality, version history, and editor session management. It coordinates between the content type registry, auto-save manager, and soft delete manager to provide a unified editing experience.
Features
- Editor Session Management: Start and end editor sessions with state tracking
- Auto-Save Coordination: Integrate with auto-save manager for seamless saving
- Soft Delete Support: Handle soft delete operations with state management
- Version History: Track content versions with metadata
- Content Validation: Validate content against content type schemas
- Event System: Comprehensive event system for editor operations
- Statistics Tracking: Track usage statistics and performance metrics
- TypeScript Support: Full type safety and IntelliSense
Installation
npm install @bernierllc/content-editor-serviceQuick Start
import { ContentEditorService } from '@bernierllc/content-editor-service';
// Create editor service
const service = new ContentEditorService({
enabled: true,
autoSave: {
enabled: true,
debounceMs: 1000,
storageMode: 'both'
},
softDelete: {
enabled: true,
showDeletedToUsers: false
},
versionHistory: {
enabled: true,
maxVersions: 50
}
});
// Initialize the service
await service.initialize();
// Start an editor session
const editorState = await service.startEditorSession({
contentId: 'content-1',
contentTypeId: 'text',
userId: 'user-123',
userPermissions: ['content.edit'],
contentMetadata: { type: 'text', title: 'My Article' }
});
// Save content
const saveResult = await service.saveContent({
contentId: 'content-1',
contentTypeId: 'text',
contentData: {
id: 'content-1',
data: { title: 'My Article', content: 'Article content...' },
metadata: { type: 'text' },
lastModified: new Date(),
version: 1
},
saveType: 'draft',
userId: 'user-123',
reason: 'Saving draft',
createVersion: true
});
if (saveResult.success) {
console.log('Content saved successfully');
}
// End the editor session
await service.endEditorSession(editorState.session.id);API Reference
ContentEditorService
The main service class for managing content editing operations.
Constructor
new ContentEditorService(config?: Partial<ContentEditorServiceConfig>)Configuration Options
interface ContentEditorServiceConfig {
enabled: boolean; // Whether the service is enabled
autoSave: {
enabled: boolean; // Whether auto-save is enabled
debounceMs: number; // Debounce delay in milliseconds
storageMode: 'local' | 'server' | 'both'; // Storage mode
};
softDelete: {
enabled: boolean; // Whether soft delete is enabled
showDeletedToUsers: boolean; // Whether to show deleted content to users
};
versionHistory: {
enabled: boolean; // Whether to track version history
maxVersions: number; // Maximum number of versions to keep
};
validation: {
enabled: boolean; // Whether to validate content
validateOnAutoSave: boolean; // Whether to validate on auto-save
};
}Methods
initialize()
Initialize the service and set up storage adapters.
await service.initialize();startEditorSession(context)
Start a new editor session for content editing.
const editorState = await service.startEditorSession({
contentId: 'content-1',
contentTypeId: 'text',
userId: 'user-123',
userPermissions: ['content.edit'],
contentMetadata: { type: 'text', title: 'My Article' }
});endEditorSession(sessionId)
End an editor session and clean up resources.
await service.endEditorSession(sessionId);saveContent(operation)
Save content with optional version creation.
const result = await service.saveContent({
contentId: 'content-1',
contentTypeId: 'text',
contentData: contentData,
saveType: 'draft', // 'draft' | 'publish' | 'auto-save'
userId: 'user-123',
reason: 'Saving draft',
createVersion: true
});loadContent(operation)
Load content with optional version specification.
const result = await service.loadContent({
contentId: 'content-1',
contentTypeId: 'text',
userId: 'user-123',
includeDeleted: false,
version: 5 // Optional specific version
});softDeleteContent(contentId, userId, reason?)
Soft delete content.
const success = await service.softDeleteContent('content-1', 'user-123', 'Content no longer needed');restoreContent(contentId, userId, reason?)
Restore soft deleted content.
const success = await service.restoreContent('content-1', 'user-123', 'Content restored');getEditorState(sessionId)
Get current editor state for a session.
const state = service.getEditorState(sessionId);
console.log('Current content:', state.contentData);
console.log('Auto-save enabled:', state.autoSaveState.enabled);
console.log('Has unsaved changes:', state.autoSaveState.hasUnsavedChanges);Event System
Listen to editor service events.
service.onEvent((event) => {
switch (event.type) {
case 'session_started':
console.log('Editor session started:', event.data.sessionId);
break;
case 'content_saved':
console.log('Content saved:', event.contentId);
break;
case 'content_auto_saved':
console.log('Content auto-saved:', event.contentId);
break;
case 'content_deleted':
console.log('Content deleted:', event.contentId);
break;
case 'version_created':
console.log('Version created:', event.data.version);
break;
}
});Hooks System
Add custom processing for editor events.
service.addHook({
name: 'audit-log',
hook: async (event) => {
// Log editor events for audit
auditLogger.log(event.type, {
timestamp: event.timestamp,
contentId: event.contentId,
userId: event.userId,
data: event.data
});
},
priority: 1,
enabled: true,
eventTypes: ['content_saved', 'content_deleted', 'version_created']
});Examples
Basic Content Editor
import { ContentEditorService } from '@bernierllc/content-editor-service';
class ContentEditor {
private service: ContentEditorService;
private currentSession: string | null = null;
constructor() {
this.service = new ContentEditorService({
enabled: true,
autoSave: {
enabled: true,
debounceMs: 1000,
storageMode: 'both'
},
softDelete: {
enabled: true,
showDeletedToUsers: false
},
versionHistory: {
enabled: true,
maxVersions: 50
}
});
// Set up event handling
this.service.onEvent((event) => {
this.handleEditorEvent(event);
});
}
async initialize() {
await this.service.initialize();
}
async openContent(contentId: string, contentTypeId: string, userId: string) {
// Start editor session
const editorState = await this.service.startEditorSession({
contentId,
contentTypeId,
userId,
userPermissions: await this.getUserPermissions(userId),
contentMetadata: await this.getContentMetadata(contentId)
});
this.currentSession = editorState.session.id;
// Load content
const loadResult = await this.service.loadContent({
contentId,
contentTypeId,
userId
});
if (loadResult.success) {
return {
contentData: loadResult.contentData,
contentType: loadResult.contentType,
editorState
};
} else {
throw new Error(`Failed to load content: ${loadResult.error}`);
}
}
async saveContent(contentData: any, saveType: 'draft' | 'publish' = 'draft') {
if (!this.currentSession) {
throw new Error('No active editor session');
}
const editorState = this.service.getEditorState(this.currentSession);
if (!editorState) {
throw new Error('Editor session not found');
}
const result = await this.service.saveContent({
contentId: editorState.contentId,
contentTypeId: editorState.contentTypeId,
contentData,
saveType,
userId: editorState.session.userId,
reason: `Saving as ${saveType}`,
createVersion: saveType === 'publish'
});
if (result.success) {
// Update UI to show saved status
this.updateSaveStatus('saved');
} else {
// Show error to user
this.showError(`Save failed: ${result.error}`);
}
return result;
}
async closeContent() {
if (this.currentSession) {
await this.service.endEditorSession(this.currentSession);
this.currentSession = null;
}
}
private handleEditorEvent(event: any) {
switch (event.type) {
case 'content_auto_saved':
this.updateSaveStatus('auto-saved');
break;
case 'content_saved':
this.updateSaveStatus('saved');
break;
case 'session_ended':
this.currentSession = null;
break;
}
}
private updateSaveStatus(status: string) {
// Update UI to show save status
console.log(`Save status: ${status}`);
}
private showError(message: string) {
// Show error to user
console.error(message);
}
}Advanced Editor with Validation
class ValidatedContentEditor extends ContentEditor {
constructor() {
super();
// Enable validation
this.service.updateConfig({
validation: {
enabled: true,
validateOnAutoSave: false // Only validate on manual saves
}
});
}
async saveContent(contentData: any, saveType: 'draft' | 'publish' = 'draft') {
if (!this.currentSession) {
throw new Error('No active editor session');
}
const editorState = this.service.getEditorState(this.currentSession);
if (!editorState) {
throw new Error('Editor session not found');
}
// Validate content before saving
const validation = await this.validateContent(contentData, editorState.contentTypeId);
if (!validation.valid) {
return {
success: false,
error: `Validation failed: ${validation.errors.join(', ')}`,
validationErrors: validation.errors
};
}
// Proceed with save
return super.saveContent(validation.validatedData || contentData, saveType);
}
private async validateContent(contentData: any, contentTypeId: string) {
// Custom validation logic
const errors: string[] = [];
const warnings: string[] = [];
// Check required fields
if (!contentData.data.title) {
errors.push('Title is required');
}
if (!contentData.data.content) {
errors.push('Content is required');
}
// Check content length
if (contentData.data.content && contentData.data.content.length < 100) {
warnings.push('Content is quite short');
}
return {
valid: errors.length === 0,
errors,
warnings,
validatedData: errors.length === 0 ? contentData : undefined
};
}
}Editor with Soft Delete Management
class SoftDeleteAwareEditor extends ContentEditor {
async deleteContent(contentId: string, userId: string, reason: string) {
const success = await this.service.softDeleteContent(contentId, userId, reason);
if (success) {
// Update UI to show deleted status
this.updateContentStatus(contentId, 'deleted');
// Show confirmation to user
this.showMessage('Content has been deleted');
} else {
this.showError('Failed to delete content');
}
return success;
}
async restoreContent(contentId: string, userId: string, reason: string) {
const success = await this.service.restoreContent(contentId, userId, reason);
if (success) {
// Update UI to show restored status
this.updateContentStatus(contentId, 'active');
// Show confirmation to user
this.showMessage('Content has been restored');
} else {
this.showError('Failed to restore content');
}
return success;
}
async loadContentWithDeleted(contentId: string, contentTypeId: string, userId: string) {
const loadResult = await this.service.loadContent({
contentId,
contentTypeId,
userId,
includeDeleted: true // Include soft deleted content
});
if (loadResult.success && loadResult.softDeleteState?.isDeleted) {
// Show warning that content is deleted
this.showWarning('This content has been deleted');
}
return loadResult;
}
}Editor with Version History
class VersionAwareEditor extends ContentEditor {
async getVersionHistory(contentId: string) {
// In a real implementation, this would fetch from a version store
// For now, return mock data
return [
{
version: 1,
timestamp: new Date('2023-01-01'),
userId: 'user-1',
description: 'Initial version',
contentData: { /* ... */ }
},
{
version: 2,
timestamp: new Date('2023-01-02'),
userId: 'user-1',
description: 'Added introduction',
contentData: { /* ... */ }
}
];
}
async loadVersion(contentId: string, version: number) {
const loadResult = await this.service.loadContent({
contentId,
contentTypeId: 'text', // Would need to get this from somewhere
userId: 'current-user',
version
});
if (loadResult.success) {
// Show warning that this is a historical version
this.showWarning(`Loading version ${version} from ${loadResult.version?.timestamp}`);
}
return loadResult;
}
async createVersion(contentId: string, userId: string, description: string) {
const editorState = this.service.getEditorState(this.currentSession!);
if (!editorState) {
throw new Error('No active editor session');
}
const result = await this.service.saveContent({
contentId,
contentTypeId: editorState.contentTypeId,
contentData: editorState.contentData,
saveType: 'draft',
userId,
reason: description,
createVersion: true
});
if (result.success) {
this.showMessage(`Version ${result.version} created: ${description}`);
}
return result;
}
}Editor Statistics and Analytics
class AnalyticsEnabledEditor extends ContentEditor {
getEditorStatistics() {
const stats = this.service.getStatistics();
return {
totalContent: stats.totalContent,
activeSessions: stats.activeSessions,
contentByType: stats.contentByType,
autoSaveStats: {
totalAutoSaves: stats.autoSaveStats.totalAutoSaves,
successRate: stats.autoSaveStats.successfulAutoSaves / stats.autoSaveStats.totalAutoSaves,
averageTime: stats.autoSaveStats.averageAutoSaveTime
},
versionStats: {
totalVersions: stats.versionStats.totalVersions,
averageVersionsPerContent: stats.versionStats.averageVersionsPerContent
},
softDeleteStats: {
totalDeleted: stats.softDeleteStats.totalSoftDeleted,
totalRestored: stats.softDeleteStats.totalRestored
}
};
}
getContentTypeUsage() {
const stats = this.service.getStatistics();
return Object.entries(stats.contentByType).map(([typeId, count]) => ({
typeId,
count,
percentage: (count / stats.totalContent) * 100
}));
}
getAutoSavePerformance() {
const stats = this.service.getStatistics();
return {
totalAutoSaves: stats.autoSaveStats.totalAutoSaves,
successfulAutoSaves: stats.autoSaveStats.successfulAutoSaves,
failedAutoSaves: stats.autoSaveStats.failedAutoSaves,
successRate: stats.autoSaveStats.successfulAutoSaves / stats.autoSaveStats.totalAutoSaves,
averageTime: stats.autoSaveStats.averageAutoSaveTime
};
}
}TypeScript Support
This package is written in TypeScript and provides full type definitions. All interfaces and types are exported for use in your own code.
License
UNLICENSED - See LICENSE file for details.
Contributing
Contributions are welcome! Please see the contributing guidelines for more information.
Support
For support and questions, please open an issue on GitHub.
