Skip to content

Commit 92974e3

Browse files
Claudehotlong
andauthored
Add REST API endpoints for metadata history operations
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 5298d35 commit 92974e3

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

packages/metadata/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export { DatabaseLoader, type DatabaseLoaderOptions } from './loaders/database-l
2121

2222
// Objects
2323
export { SysMetadataObject } from './objects/sys-metadata.object.js';
24+
export { SysMetadataHistoryObject } from './objects/sys-metadata-history.object.js';
25+
26+
// Routes
27+
export { registerMetadataHistoryRoutes } from './routes/history-routes.js';
28+
29+
// Utils
30+
export { calculateChecksum, generateSimpleDiff, generateDiffSummary } from './utils/metadata-history-utils.js';
2431

2532
// Serializers
2633
export { type MetadataSerializer, type SerializeOptions } from './serializers/serializer-interface.js';
@@ -43,6 +50,11 @@ export type {
4350
MetadataCollectionInfo,
4451
MetadataLoaderContract,
4552
MetadataManagerConfig,
53+
MetadataHistoryRecord,
54+
MetadataHistoryQueryOptions,
55+
MetadataHistoryQueryResult,
56+
MetadataDiffResult,
57+
MetadataHistoryRetentionPolicy,
4658
} from '@objectstack/spec/system';
4759

4860
// Re-export IMetadataService contract
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Metadata History API Routes
5+
*
6+
* REST API endpoints for metadata version history, rollback, and diff operations.
7+
* These routes extend the standard metadata API with history-specific functionality.
8+
*
9+
* Routes:
10+
* - GET /api/v1/metadata/:type/:name/history - Get version history
11+
* - POST /api/v1/metadata/:type/:name/rollback - Rollback to a specific version
12+
* - GET /api/v1/metadata/:type/:name/diff - Compare two versions
13+
*/
14+
15+
import type { IMetadataService } from '@objectstack/spec/contracts';
16+
17+
/**
18+
* Register metadata history routes on a Hono app or any HTTP server.
19+
*
20+
* @param app - The HTTP server/router instance (Hono-compatible)
21+
* @param metadataService - The metadata service instance
22+
*/
23+
export function registerMetadataHistoryRoutes(
24+
app: any, // Hono app or compatible
25+
metadataService: IMetadataService
26+
): void {
27+
/**
28+
* GET /api/v1/metadata/:type/:name/history
29+
* Get version history for a metadata item
30+
*
31+
* Query parameters:
32+
* - limit: number (default: 50)
33+
* - offset: number (default: 0)
34+
* - since: ISO datetime string
35+
* - until: ISO datetime string
36+
* - operationType: create | update | publish | revert | delete
37+
* - includeMetadata: boolean (default: true)
38+
*/
39+
app.get('/api/v1/metadata/:type/:name/history', async (c: any) => {
40+
if (!metadataService.getHistory) {
41+
return c.json({ error: 'History tracking not enabled' }, 501);
42+
}
43+
44+
const { type, name } = c.req.param();
45+
const query = c.req.query();
46+
47+
try {
48+
const options: any = {};
49+
50+
if (query.limit) options.limit = parseInt(query.limit, 10);
51+
if (query.offset) options.offset = parseInt(query.offset, 10);
52+
if (query.since) options.since = query.since;
53+
if (query.until) options.until = query.until;
54+
if (query.operationType) options.operationType = query.operationType;
55+
if (query.includeMetadata !== undefined) {
56+
options.includeMetadata = query.includeMetadata === 'true';
57+
}
58+
59+
const result = await metadataService.getHistory(type, name, options);
60+
61+
return c.json({
62+
success: true,
63+
data: result,
64+
});
65+
} catch (error) {
66+
return c.json(
67+
{
68+
success: false,
69+
error: error instanceof Error ? error.message : 'Failed to retrieve history',
70+
},
71+
500
72+
);
73+
}
74+
});
75+
76+
/**
77+
* POST /api/v1/metadata/:type/:name/rollback
78+
* Rollback a metadata item to a specific version
79+
*
80+
* Body:
81+
* - version: number (required) - Target version to rollback to
82+
* - changeNote: string (optional) - Description of rollback
83+
* - recordedBy: string (optional) - User performing rollback
84+
*/
85+
app.post('/api/v1/metadata/:type/:name/rollback', async (c: any) => {
86+
if (!metadataService.rollback) {
87+
return c.json({ error: 'Rollback not supported' }, 501);
88+
}
89+
90+
const { type, name } = c.req.param();
91+
92+
try {
93+
const body = await c.req.json();
94+
const { version, changeNote, recordedBy } = body;
95+
96+
if (typeof version !== 'number') {
97+
return c.json(
98+
{
99+
success: false,
100+
error: 'Version number is required',
101+
},
102+
400
103+
);
104+
}
105+
106+
const restoredMetadata = await metadataService.rollback(type, name, version, {
107+
changeNote,
108+
recordedBy,
109+
});
110+
111+
return c.json({
112+
success: true,
113+
data: {
114+
type,
115+
name,
116+
version,
117+
metadata: restoredMetadata,
118+
},
119+
});
120+
} catch (error) {
121+
return c.json(
122+
{
123+
success: false,
124+
error: error instanceof Error ? error.message : 'Rollback failed',
125+
},
126+
500
127+
);
128+
}
129+
});
130+
131+
/**
132+
* GET /api/v1/metadata/:type/:name/diff
133+
* Compare two versions of a metadata item
134+
*
135+
* Query parameters:
136+
* - version1: number (required) - First version (older)
137+
* - version2: number (required) - Second version (newer)
138+
*/
139+
app.get('/api/v1/metadata/:type/:name/diff', async (c: any) => {
140+
if (!metadataService.diff) {
141+
return c.json({ error: 'Diff not supported' }, 501);
142+
}
143+
144+
const { type, name } = c.req.param();
145+
const query = c.req.query();
146+
147+
try {
148+
const version1 = parseInt(query.version1, 10);
149+
const version2 = parseInt(query.version2, 10);
150+
151+
if (isNaN(version1) || isNaN(version2)) {
152+
return c.json(
153+
{
154+
success: false,
155+
error: 'Both version1 and version2 query parameters are required',
156+
},
157+
400
158+
);
159+
}
160+
161+
const diffResult = await metadataService.diff(type, name, version1, version2);
162+
163+
return c.json({
164+
success: true,
165+
data: diffResult,
166+
});
167+
} catch (error) {
168+
return c.json(
169+
{
170+
success: false,
171+
error: error instanceof Error ? error.message : 'Diff failed',
172+
},
173+
500
174+
);
175+
}
176+
});
177+
}

0 commit comments

Comments
 (0)