The ObjectStack metadata system now supports comprehensive version history tracking and rollback capabilities. This feature allows administrators to:
- View complete change history for any metadata item
- Compare versions to see what changed
- Rollback to previous versions when needed
- Track who made changes and when
- Automatically clean up old history records
-
MetadataHistoryRecordSchema (
packages/spec/src/system/metadata-persistence.zod.ts)- Defines the structure for history records
- Includes version, checksum, operation type, change notes, and audit fields
-
sys_metadata_history Object (
packages/metadata/src/objects/sys-metadata-history.object.ts)- System table for storing historical snapshots
- Automatically created when history tracking is enabled
-
DatabaseLoader History Tracking (
packages/metadata/src/loaders/database-loader.ts)- Automatically writes history records on create/update
- Calculates SHA-256 checksums for change detection
- Skips duplicate history records when content is unchanged
-
MetadataManager History Methods (
packages/metadata/src/metadata-manager.ts)getHistory()- Query version timelinerollback()- Restore previous versiondiff()- Compare two versions
-
REST API Endpoints (
packages/metadata/src/routes/history-routes.ts)GET /api/v1/metadata/:type/:name/history- View historyPOST /api/v1/metadata/:type/:name/rollback- Rollback to versionGET /api/v1/metadata/:type/:name/diff- Compare versions
-
Cleanup Manager (
packages/metadata/src/utils/history-cleanup.ts)- Age-based retention (maxAgeDays)
- Count-based retention (maxVersions)
- Automatic scheduled cleanup
History tracking is enabled by default when using a DatabaseLoader:
import { DatabaseLoader } from '@objectstack/metadata';
const dbLoader = new DatabaseLoader({
driver: myDriver,
trackHistory: true, // Default: true
});const history = await metadataService.getHistory('object', 'account', {
limit: 50,
offset: 0,
operationType: 'update',
since: '2025-01-01T00:00:00Z',
});
console.log(`Total versions: ${history.total}`);
history.records.forEach(record => {
console.log(`Version ${record.version} - ${record.operationType} by ${record.recordedBy} at ${record.recordedAt}`);
});const restored = await metadataService.rollback('object', 'account', 5, {
changeNote: 'Reverting problematic changes',
recordedBy: 'admin@example.com',
});const diff = await metadataService.diff('object', 'account', 5, 6);
console.log(`Identical: ${diff.identical}`);
console.log(`Summary: ${diff.summary}`);
console.log(`Changes: ${diff.patch.length} operations`);
diff.patch.forEach(op => {
console.log(`${op.op} ${op.path}: ${JSON.stringify(op.value)}`);
});import { HistoryCleanupManager } from '@objectstack/metadata';
const cleanupManager = new HistoryCleanupManager(
{
maxVersions: 100, // Keep last 100 versions per item
maxAgeDays: 180, // Keep history for 6 months
autoCleanup: true, // Enable automatic cleanup
cleanupIntervalHours: 24, // Run daily
},
dbLoader
);
// Start automatic cleanup
cleanupManager.start();
// Manual cleanup
const result = await cleanupManager.runCleanup();
console.log(`Deleted ${result.deleted} records, ${result.errors} errors`);
// Preview cleanup
const stats = await cleanupManager.getCleanupStats();
console.log(`Would delete ${stats.total} records`);
// Stop cleanup
cleanupManager.stop();# Get history
curl "http://localhost:3000/api/v1/metadata/object/account/history?limit=10"
# Rollback to version 5
curl -X POST "http://localhost:3000/api/v1/metadata/object/account/rollback" \
-H "Content-Type: application/json" \
-d '{"version": 5, "changeNote": "Reverting changes"}'
# Compare versions
curl "http://localhost:3000/api/v1/metadata/object/account/diff?version1=5&version2=6"In your Hono app:
import { registerMetadataHistoryRoutes } from '@objectstack/metadata';
registerMetadataHistoryRoutes(app, metadataService);- Uses SHA-256 hashing of normalized JSON
- Keys are sorted recursively for deterministic output
- Fallback to simple hash for environments without Web Crypto API
{
id: string; // UUID
metadataId: string; // FK to sys_metadata.id
name: string; // Denormalized for queries
type: string; // Denormalized for queries
version: number; // Version number
operationType: 'create' | 'update' | 'publish' | 'revert' | 'delete';
metadata: Record<string, unknown>; // Full snapshot
checksum: string; // SHA-256 hash
previousChecksum?: string; // For diff optimization
changeNote?: string; // Human-readable description
tenantId?: string; // Multi-tenant isolation
recordedBy?: string; // User identifier
recordedAt: string; // ISO datetime
}The sys_metadata_history table includes indexes for:
(metadata_id, version)- Unique constraint(metadata_id, recorded_at)- Timeline queries(type, name)- Cross-type queries(recorded_at)- Age-based cleanup(operation_type)- Operation filtering(tenant_id)- Multi-tenant isolation
Uses a simple recursive diff algorithm that generates operations:
add- New field addedremove- Field removedreplace- Field value changed
The diff result includes:
- Array of change operations
- Human-readable summary (e.g., "2 fields added, 1 field modified")
- History records are written asynchronously and failures don't block main operations
- Checksum deduplication prevents storing identical versions
- Indexes optimize common query patterns
- Automatic cleanup prevents unbounded growth
- History queries default to 50 records with pagination
| Platform | Version Control | History Duration |
|---|---|---|
| Salesforce | Setup Audit Trail | 6 months |
| ServiceNow | Update Sets + Versions | Unlimited |
| ObjectStack | sys_metadata_history | Configurable |
- Export/import history bundles
- Visual diff UI in Studio
- Change request workflow integration
- History annotations and tagging
- Merge conflict resolution
- Branch/fork metadata workflow