|
| 1 | +# Metadata Versioning & History |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The ObjectStack metadata system now supports comprehensive version history tracking and rollback capabilities. This feature allows administrators to: |
| 6 | + |
| 7 | +- View complete change history for any metadata item |
| 8 | +- Compare versions to see what changed |
| 9 | +- Rollback to previous versions when needed |
| 10 | +- Track who made changes and when |
| 11 | +- Automatically clean up old history records |
| 12 | + |
| 13 | +## Architecture |
| 14 | + |
| 15 | +### Core Components |
| 16 | + |
| 17 | +1. **MetadataHistoryRecordSchema** (`packages/spec/src/system/metadata-persistence.zod.ts`) |
| 18 | + - Defines the structure for history records |
| 19 | + - Includes version, checksum, operation type, change notes, and audit fields |
| 20 | + |
| 21 | +2. **sys_metadata_history Object** (`packages/metadata/src/objects/sys-metadata-history.object.ts`) |
| 22 | + - System table for storing historical snapshots |
| 23 | + - Automatically created when history tracking is enabled |
| 24 | + |
| 25 | +3. **DatabaseLoader History Tracking** (`packages/metadata/src/loaders/database-loader.ts`) |
| 26 | + - Automatically writes history records on create/update |
| 27 | + - Calculates SHA-256 checksums for change detection |
| 28 | + - Skips duplicate history records when content is unchanged |
| 29 | + |
| 30 | +4. **MetadataManager History Methods** (`packages/metadata/src/metadata-manager.ts`) |
| 31 | + - `getHistory()` - Query version timeline |
| 32 | + - `rollback()` - Restore previous version |
| 33 | + - `diff()` - Compare two versions |
| 34 | + |
| 35 | +5. **REST API Endpoints** (`packages/metadata/src/routes/history-routes.ts`) |
| 36 | + - `GET /api/v1/metadata/:type/:name/history` - View history |
| 37 | + - `POST /api/v1/metadata/:type/:name/rollback` - Rollback to version |
| 38 | + - `GET /api/v1/metadata/:type/:name/diff` - Compare versions |
| 39 | + |
| 40 | +6. **Cleanup Manager** (`packages/metadata/src/utils/history-cleanup.ts`) |
| 41 | + - Age-based retention (maxAgeDays) |
| 42 | + - Count-based retention (maxVersions) |
| 43 | + - Automatic scheduled cleanup |
| 44 | + |
| 45 | +## Usage |
| 46 | + |
| 47 | +### Enable History Tracking |
| 48 | + |
| 49 | +History tracking is enabled by default when using a DatabaseLoader: |
| 50 | + |
| 51 | +```typescript |
| 52 | +import { DatabaseLoader } from '@objectstack/metadata'; |
| 53 | + |
| 54 | +const dbLoader = new DatabaseLoader({ |
| 55 | + driver: myDriver, |
| 56 | + trackHistory: true, // Default: true |
| 57 | +}); |
| 58 | +``` |
| 59 | + |
| 60 | +### Query Version History |
| 61 | + |
| 62 | +```typescript |
| 63 | +const history = await metadataService.getHistory('object', 'account', { |
| 64 | + limit: 50, |
| 65 | + offset: 0, |
| 66 | + operationType: 'update', |
| 67 | + since: '2025-01-01T00:00:00Z', |
| 68 | +}); |
| 69 | + |
| 70 | +console.log(`Total versions: ${history.total}`); |
| 71 | +history.records.forEach(record => { |
| 72 | + console.log(`Version ${record.version} - ${record.operationType} by ${record.recordedBy} at ${record.recordedAt}`); |
| 73 | +}); |
| 74 | +``` |
| 75 | + |
| 76 | +### Rollback to Previous Version |
| 77 | + |
| 78 | +```typescript |
| 79 | +const restored = await metadataService.rollback('object', 'account', 5, { |
| 80 | + changeNote: 'Reverting problematic changes', |
| 81 | + recordedBy: 'admin@example.com', |
| 82 | +}); |
| 83 | +``` |
| 84 | + |
| 85 | +### Compare Versions |
| 86 | + |
| 87 | +```typescript |
| 88 | +const diff = await metadataService.diff('object', 'account', 5, 6); |
| 89 | + |
| 90 | +console.log(`Identical: ${diff.identical}`); |
| 91 | +console.log(`Summary: ${diff.summary}`); |
| 92 | +console.log(`Changes: ${diff.patch.length} operations`); |
| 93 | + |
| 94 | +diff.patch.forEach(op => { |
| 95 | + console.log(`${op.op} ${op.path}: ${JSON.stringify(op.value)}`); |
| 96 | +}); |
| 97 | +``` |
| 98 | + |
| 99 | +### Configure Retention Policy |
| 100 | + |
| 101 | +```typescript |
| 102 | +import { HistoryCleanupManager } from '@objectstack/metadata'; |
| 103 | + |
| 104 | +const cleanupManager = new HistoryCleanupManager( |
| 105 | + { |
| 106 | + maxVersions: 100, // Keep last 100 versions per item |
| 107 | + maxAgeDays: 180, // Keep history for 6 months |
| 108 | + autoCleanup: true, // Enable automatic cleanup |
| 109 | + cleanupIntervalHours: 24, // Run daily |
| 110 | + }, |
| 111 | + dbLoader |
| 112 | +); |
| 113 | + |
| 114 | +// Start automatic cleanup |
| 115 | +cleanupManager.start(); |
| 116 | + |
| 117 | +// Manual cleanup |
| 118 | +const result = await cleanupManager.runCleanup(); |
| 119 | +console.log(`Deleted ${result.deleted} records, ${result.errors} errors`); |
| 120 | + |
| 121 | +// Preview cleanup |
| 122 | +const stats = await cleanupManager.getCleanupStats(); |
| 123 | +console.log(`Would delete ${stats.total} records`); |
| 124 | + |
| 125 | +// Stop cleanup |
| 126 | +cleanupManager.stop(); |
| 127 | +``` |
| 128 | + |
| 129 | +### REST API Examples |
| 130 | + |
| 131 | +```bash |
| 132 | +# Get history |
| 133 | +curl "http://localhost:3000/api/v1/metadata/object/account/history?limit=10" |
| 134 | + |
| 135 | +# Rollback to version 5 |
| 136 | +curl -X POST "http://localhost:3000/api/v1/metadata/object/account/rollback" \ |
| 137 | + -H "Content-Type: application/json" \ |
| 138 | + -d '{"version": 5, "changeNote": "Reverting changes"}' |
| 139 | + |
| 140 | +# Compare versions |
| 141 | +curl "http://localhost:3000/api/v1/metadata/object/account/diff?version1=5&version2=6" |
| 142 | +``` |
| 143 | + |
| 144 | +### Register History Routes |
| 145 | + |
| 146 | +In your Hono app: |
| 147 | + |
| 148 | +```typescript |
| 149 | +import { registerMetadataHistoryRoutes } from '@objectstack/metadata'; |
| 150 | + |
| 151 | +registerMetadataHistoryRoutes(app, metadataService); |
| 152 | +``` |
| 153 | + |
| 154 | +## Implementation Details |
| 155 | + |
| 156 | +### Checksum Calculation |
| 157 | + |
| 158 | +- Uses SHA-256 hashing of normalized JSON |
| 159 | +- Keys are sorted recursively for deterministic output |
| 160 | +- Fallback to simple hash for environments without Web Crypto API |
| 161 | + |
| 162 | +### History Record Structure |
| 163 | + |
| 164 | +```typescript |
| 165 | +{ |
| 166 | + id: string; // UUID |
| 167 | + metadataId: string; // FK to sys_metadata.id |
| 168 | + name: string; // Denormalized for queries |
| 169 | + type: string; // Denormalized for queries |
| 170 | + version: number; // Version number |
| 171 | + operationType: 'create' | 'update' | 'publish' | 'revert' | 'delete'; |
| 172 | + metadata: Record<string, unknown>; // Full snapshot |
| 173 | + checksum: string; // SHA-256 hash |
| 174 | + previousChecksum?: string; // For diff optimization |
| 175 | + changeNote?: string; // Human-readable description |
| 176 | + tenantId?: string; // Multi-tenant isolation |
| 177 | + recordedBy?: string; // User identifier |
| 178 | + recordedAt: string; // ISO datetime |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +### Database Schema |
| 183 | + |
| 184 | +The `sys_metadata_history` table includes indexes for: |
| 185 | +- `(metadata_id, version)` - Unique constraint |
| 186 | +- `(metadata_id, recorded_at)` - Timeline queries |
| 187 | +- `(type, name)` - Cross-type queries |
| 188 | +- `(recorded_at)` - Age-based cleanup |
| 189 | +- `(operation_type)` - Operation filtering |
| 190 | +- `(tenant_id)` - Multi-tenant isolation |
| 191 | + |
| 192 | +### Diff Algorithm |
| 193 | + |
| 194 | +Uses a simple recursive diff algorithm that generates operations: |
| 195 | +- `add` - New field added |
| 196 | +- `remove` - Field removed |
| 197 | +- `replace` - Field value changed |
| 198 | + |
| 199 | +The diff result includes: |
| 200 | +- Array of change operations |
| 201 | +- Human-readable summary (e.g., "2 fields added, 1 field modified") |
| 202 | + |
| 203 | +## Performance Considerations |
| 204 | + |
| 205 | +- History records are written synchronously as part of each save operation, ensuring consistency between metadata state and the history timeline |
| 206 | +- Checksum deduplication prevents storing identical versions |
| 207 | +- Indexes optimize common query patterns |
| 208 | +- Automatic cleanup prevents unbounded growth |
| 209 | +- History queries default to 50 records with pagination |
| 210 | + |
| 211 | +## Comparison with Other Platforms |
| 212 | + |
| 213 | +| Platform | Version Control | History Duration | |
| 214 | +|----------|----------------|------------------| |
| 215 | +| Salesforce | Setup Audit Trail | 6 months | |
| 216 | +| ServiceNow | Update Sets + Versions | Unlimited | |
| 217 | +| ObjectStack | sys_metadata_history | Configurable | |
| 218 | + |
| 219 | +## Future Enhancements |
| 220 | + |
| 221 | +- [ ] Export/import history bundles |
| 222 | +- [ ] Visual diff UI in Studio |
| 223 | +- [ ] Change request workflow integration |
| 224 | +- [ ] History annotations and tagging |
| 225 | +- [ ] Merge conflict resolution |
| 226 | +- [ ] Branch/fork metadata workflow |
0 commit comments