Skip to content

Commit f054641

Browse files
Claudehotlong
andauthored
Add retention policy, cleanup manager, and comprehensive tests
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/f60fe20f-aa2a-44ef-9eea-fb72d6cc454f Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 92974e3 commit f054641

3 files changed

Lines changed: 470 additions & 0 deletions

File tree

packages/metadata/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { registerMetadataHistoryRoutes } from './routes/history-routes.js';
2828

2929
// Utils
3030
export { calculateChecksum, generateSimpleDiff, generateDiffSummary } from './utils/metadata-history-utils.js';
31+
export { HistoryCleanupManager } from './utils/history-cleanup.js';
3132

3233
// Serializers
3334
export { type MetadataSerializer, type SerializeOptions } from './serializers/serializer-interface.js';
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { MetadataManager } from '../metadata-manager.js';
5+
import { DatabaseLoader } from '../loaders/database-loader.js';
6+
import { MemoryDriver } from '@objectstack/driver-memory';
7+
8+
describe('Metadata History', () => {
9+
let manager: MetadataManager;
10+
let driver: MemoryDriver;
11+
12+
beforeEach(async () => {
13+
// Create a fresh in-memory driver and database loader
14+
driver = new MemoryDriver({});
15+
16+
const dbLoader = new DatabaseLoader({
17+
driver,
18+
tableName: 'test_metadata',
19+
historyTableName: 'test_metadata_history',
20+
trackHistory: true,
21+
});
22+
23+
manager = new MetadataManager({
24+
datasource: 'memory',
25+
loaders: [dbLoader],
26+
});
27+
});
28+
29+
it('should create history record on metadata creation', async () => {
30+
// Register a new metadata item
31+
const objectDef = {
32+
name: 'test_object',
33+
label: 'Test Object',
34+
fields: {
35+
name: { type: 'text', label: 'Name' },
36+
},
37+
};
38+
39+
await manager.register('object', 'test_object', objectDef);
40+
41+
// Check that history was created
42+
if (manager.getHistory) {
43+
const history = await manager.getHistory('object', 'test_object');
44+
45+
expect(history.records.length).toBeGreaterThan(0);
46+
expect(history.records[0].operationType).toBe('create');
47+
expect(history.records[0].version).toBe(1);
48+
}
49+
});
50+
51+
it('should create history record on metadata update', async () => {
52+
// Register initial version
53+
const objectDef = {
54+
name: 'test_object',
55+
label: 'Test Object',
56+
fields: {
57+
name: { type: 'text', label: 'Name' },
58+
},
59+
};
60+
61+
await manager.register('object', 'test_object', objectDef);
62+
63+
// Update the metadata
64+
const updatedDef = {
65+
...objectDef,
66+
label: 'Updated Test Object',
67+
fields: {
68+
name: { type: 'text', label: 'Name' },
69+
description: { type: 'text', label: 'Description' },
70+
},
71+
};
72+
73+
await manager.register('object', 'test_object', updatedDef);
74+
75+
// Check history
76+
if (manager.getHistory) {
77+
const history = await manager.getHistory('object', 'test_object');
78+
79+
expect(history.records.length).toBeGreaterThanOrEqual(2);
80+
expect(history.records[0].operationType).toBe('update');
81+
expect(history.records[0].version).toBe(2);
82+
}
83+
});
84+
85+
it('should rollback to previous version', async () => {
86+
// Register initial version
87+
const version1 = {
88+
name: 'test_object',
89+
label: 'Version 1',
90+
fields: {
91+
name: { type: 'text', label: 'Name' },
92+
},
93+
};
94+
95+
await manager.register('object', 'test_object', version1);
96+
97+
// Update to version 2
98+
const version2 = {
99+
...version1,
100+
label: 'Version 2',
101+
};
102+
103+
await manager.register('object', 'test_object', version2);
104+
105+
// Rollback to version 1
106+
if (manager.rollback) {
107+
const restored = await manager.rollback('object', 'test_object', 1);
108+
109+
expect(restored).toBeDefined();
110+
expect((restored as any).label).toBe('Version 1');
111+
}
112+
113+
// Verify current metadata is version 1
114+
const current = await manager.get('object', 'test_object');
115+
expect((current as any).label).toBe('Version 1');
116+
});
117+
118+
it('should compare versions with diff', async () => {
119+
// Register version 1
120+
const version1 = {
121+
name: 'test_object',
122+
label: 'Version 1',
123+
description: 'Original description',
124+
fields: {
125+
name: { type: 'text', label: 'Name' },
126+
},
127+
};
128+
129+
await manager.register('object', 'test_object', version1);
130+
131+
// Update to version 2
132+
const version2 = {
133+
...version1,
134+
label: 'Version 2',
135+
description: 'Updated description',
136+
};
137+
138+
await manager.register('object', 'test_object', version2);
139+
140+
// Compare versions
141+
if (manager.diff) {
142+
const diffResult = await manager.diff('object', 'test_object', 1, 2);
143+
144+
expect(diffResult.identical).toBe(false);
145+
expect(diffResult.patch.length).toBeGreaterThan(0);
146+
expect(diffResult.summary).toContain('modified');
147+
}
148+
});
149+
150+
it('should handle history query with filters', async () => {
151+
// Create multiple versions
152+
for (let i = 1; i <= 5; i++) {
153+
await manager.register('object', 'test_object', {
154+
name: 'test_object',
155+
label: `Version ${i}`,
156+
});
157+
158+
// Small delay to ensure different timestamps
159+
await new Promise(resolve => setTimeout(resolve, 10));
160+
}
161+
162+
if (manager.getHistory) {
163+
// Query with limit
164+
const limitedHistory = await manager.getHistory('object', 'test_object', {
165+
limit: 3,
166+
});
167+
168+
expect(limitedHistory.records.length).toBeLessThanOrEqual(3);
169+
expect(limitedHistory.total).toBeGreaterThanOrEqual(5);
170+
171+
// Query with operation type filter
172+
const createHistory = await manager.getHistory('object', 'test_object', {
173+
operationType: 'create',
174+
});
175+
176+
expect(createHistory.records.every(r => r.operationType === 'create')).toBe(true);
177+
}
178+
});
179+
180+
it('should skip history record when checksum is unchanged', async () => {
181+
// Register metadata
182+
const objectDef = {
183+
name: 'test_object',
184+
label: 'Test Object',
185+
};
186+
187+
await manager.register('object', 'test_object', objectDef);
188+
189+
// Re-register with exact same content
190+
await manager.register('object', 'test_object', objectDef);
191+
192+
if (manager.getHistory) {
193+
const history = await manager.getHistory('object', 'test_object');
194+
195+
// Should only have one history record (the create)
196+
// The second register should be skipped due to identical checksum
197+
expect(history.records.length).toBe(1);
198+
}
199+
});
200+
201+
it('should return empty history for non-existent metadata', async () => {
202+
if (manager.getHistory) {
203+
const history = await manager.getHistory('object', 'nonexistent');
204+
205+
expect(history.records).toEqual([]);
206+
expect(history.total).toBe(0);
207+
expect(history.hasMore).toBe(false);
208+
}
209+
});
210+
211+
it('should throw error when rolling back to non-existent version', async () => {
212+
await manager.register('object', 'test_object', {
213+
name: 'test_object',
214+
label: 'Test',
215+
});
216+
217+
if (manager.rollback) {
218+
await expect(
219+
manager.rollback('object', 'test_object', 999)
220+
).rejects.toThrow();
221+
}
222+
});
223+
});

0 commit comments

Comments
 (0)