Skip to content

Commit c46b4b0

Browse files
committed
feat: implement SaveMetaItem request and response schemas; add RemoteLoader for metadata fetching; enhance MetadataManager with Node-specific capabilities
1 parent 0c7eb7e commit c46b4b0

19 files changed

Lines changed: 657 additions & 115 deletions

File tree

content/docs/references/api/protocol.mdx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Architecture Alignment:
3434
## TypeScript Usage
3535

3636
```typescript
37-
import { AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetUiViewRequest, GetUiViewResponse, ObjectStackProtocol, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse } from '@objectstack/spec/api';
38-
import type { AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetUiViewRequest, GetUiViewResponse, ObjectStackProtocol, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse } from '@objectstack/spec/api';
37+
import { AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetUiViewRequest, GetUiViewResponse, ObjectStackProtocol, SaveMetaItemRequest, SaveMetaItemResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse } from '@objectstack/spec/api';
38+
import type { AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetUiViewRequest, GetUiViewResponse, ObjectStackProtocol, SaveMetaItemRequest, SaveMetaItemResponse, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse } from '@objectstack/spec/api';
3939

4040
// Validate data
4141
const result = AutomationTriggerRequest.parse(data);
@@ -411,6 +411,31 @@ const result = AutomationTriggerRequest.parse(data);
411411
| :--- | :--- | :--- | :--- |
412412

413413

414+
---
415+
416+
## SaveMetaItemRequest
417+
418+
### Properties
419+
420+
| Property | Type | Required | Description |
421+
| :--- | :--- | :--- | :--- |
422+
| **type** | `string` || Metadata type name |
423+
| **name** | `string` || Item name |
424+
| **item** | `any` | optional | Metadata item definition |
425+
426+
427+
---
428+
429+
## SaveMetaItemResponse
430+
431+
### Properties
432+
433+
| Property | Type | Required | Description |
434+
| :--- | :--- | :--- | :--- |
435+
| **success** | `boolean` || |
436+
| **message** | `string` | optional | |
437+
438+
414439
---
415440

416441
## UpdateDataRequest

examples/app-react-crud/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@objectstack/client-react": "workspace:*",
1818
"@objectstack/driver-memory": "workspace:*",
1919
"@example/app-todo": "workspace:*",
20+
"@objectstack/metadata": "workspace:*",
2021
"@objectstack/plugin-msw": "workspace:*",
2122
"@objectstack/runtime": "workspace:*",
2223
"@objectstack/objectql": "workspace:*",

examples/app-react-crud/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ObjectStackClient } from '@objectstack/client';
1010
import type { Task } from './types';
1111
import { TaskForm } from './components/TaskForm';
1212
import { TaskList } from './components/TaskList';
13+
import { MetadataDemo } from './components/MetadataDemo';
1314
import './App.css';
1415

1516
export function App() {
@@ -130,6 +131,10 @@ export function App() {
130131
</section>
131132
</main>
132133

134+
<div className="mt-8">
135+
<MetadataDemo />
136+
</div>
137+
133138
<footer className="mt-16 pt-8 border-t border-accents-2 text-center text-sm text-accents-4 space-y-2">
134139
<p>
135140
This example demonstrates MSW integration with React components.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useState, useEffect } from 'react';
2+
import { MetadataManager, RemoteLoader } from '@objectstack/metadata';
3+
4+
export function MetadataDemo() {
5+
const [manager, setManager] = useState<MetadataManager | null>(null);
6+
const [data, setData] = useState<any>(null);
7+
const [loading, setLoading] = useState(false);
8+
const [error, setError] = useState<string | null>(null);
9+
10+
useEffect(() => {
11+
// Initialize Metadata Manager with Remote Loader pointing to MSW
12+
// MSW is configured at /api/v1
13+
// MSW Plugin exposes metadata at /meta/:type/:name
14+
// So RemoteLoader should target /api/v1/meta
15+
const mgr = new MetadataManager({
16+
loaders: [
17+
new RemoteLoader('/api/v1/meta')
18+
]
19+
});
20+
setManager(mgr);
21+
}, []);
22+
23+
const loadMetadata = async () => {
24+
if (!manager) return;
25+
setLoading(true);
26+
setError(null);
27+
setData(null);
28+
29+
try {
30+
console.log('Fetching metadata for object: todo_task...');
31+
const result = await manager.load('objects', 'todo_task');
32+
console.log('Result:', result);
33+
34+
if (result) {
35+
setData(result);
36+
} else {
37+
setError('Metadata not found');
38+
}
39+
} catch (err) {
40+
console.error('Metadata load error:', err);
41+
setError(err instanceof Error ? err.message : 'Unknown error');
42+
} finally {
43+
setLoading(false);
44+
}
45+
};
46+
47+
return (
48+
<div className="p-4 border rounded bg-gray-50 mt-4">
49+
<h3 className="text-lg font-bold mb-2">Remote Loader Demo (Browser)</h3>
50+
<p className="text-sm text-gray-600 mb-4">
51+
Fetches metadata via <code>RemoteLoader</code><code>fetch</code><code>MSW Interceptor</code><code>ObjectStack Kernel</code>
52+
</p>
53+
54+
<div className="flex gap-2 mb-4">
55+
<button
56+
onClick={loadMetadata}
57+
disabled={loading || !manager}
58+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
59+
>
60+
{loading ? 'Loading...' : 'Load TodoTask Metadata'}
61+
</button>
62+
</div>
63+
64+
{error && (
65+
<div className="p-3 bg-red-100 text-red-700 rounded mb-2">
66+
{error}
67+
</div>
68+
)}
69+
70+
{data && (
71+
<div className="bg-white p-3 rounded border overflow-auto max-h-60">
72+
<pre className="text-xs">{JSON.stringify(data, null, 2)}</pre>
73+
</div>
74+
)}
75+
</div>
76+
);
77+
}

examples/metadata-demo/src/local-fs-workflow.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MetadataManager } from '@objectstack/metadata';
1+
import { NodeMetadataManager } from '@objectstack/metadata/node';
22
import * as path from 'node:path';
33
import * as fs from 'node:fs/promises';
44

@@ -10,8 +10,8 @@ async function main() {
1010
console.log(`📂 项目目录: ${PROJECT_ROOT}`);
1111

1212
// 1. 初始化管理器,指向项目根目录
13-
// MetadataManager 会自动在这个目录下寻找 `objects`, `views`, `apps` 等子目录
14-
const manager = new MetadataManager({
13+
// NodeMetadataManager 会自动在这个目录下寻找 `objects`, `views`, `apps` 等子目录
14+
const manager = new NodeMetadataManager({
1515
rootDir: path.join(PROJECT_ROOT, 'src'), // 假设元数据在 src 下
1616
formats: ['yaml', 'json'], // 优先使用 YAML
1717
watch: true // 开启监听
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
import { MetadataManager } from '@objectstack/metadata';
3+
import { RemoteLoader } from '@objectstack/metadata';
4+
5+
// Mock global fetch to simulate MSW (Mock Service Worker)
6+
// In a real app, MSW would intercept the requests network-level.
7+
const mockDatabase: Record<string, any> = {
8+
'objects/customer': {
9+
name: 'customer',
10+
label: 'Customer',
11+
fields: {
12+
name: { type: 'text' }
13+
}
14+
},
15+
'views/all_customers': {
16+
name: 'all_customers',
17+
object: 'customer',
18+
type: 'grid'
19+
}
20+
};
21+
22+
global.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
23+
const url = input.toString();
24+
console.log(`[MockNetwork] ${init?.method || 'GET'} ${url}`);
25+
26+
if (init?.method === 'GET') {
27+
// URL format: http://api.example.com/metadata/{type}/{name}
28+
const parts = url.split('/');
29+
const name = parts.pop();
30+
const type = parts.pop();
31+
const key = `${type}/${name}`;
32+
33+
if (mockDatabase[key]) {
34+
return new Response(JSON.stringify(mockDatabase[key]), { status: 200 });
35+
}
36+
return new Response(null, { status: 404 });
37+
}
38+
39+
// Handle loadMany (list)
40+
// URL format: http://api.example.com/metadata/{type}
41+
// This is a naive check, improvements needed for robust routing
42+
if (init?.method === 'GET' && !url.split('/').pop()?.includes('_')) {
43+
// Simplifying for demo: returns empty list for directory listing if not specific match above
44+
return new Response(JSON.stringify([]), { status: 200 });
45+
}
46+
47+
return new Response(null, { status: 404 });
48+
};
49+
50+
51+
async function runRemoteDemo() {
52+
console.log('--- Starting Remote Loader Demo (Simulating MSW) ---');
53+
54+
// 1. Initialize Manager with RemoteLoader
55+
const manager = new MetadataManager({
56+
loaders: [
57+
new RemoteLoader('http://api.example.com/metadata')
58+
]
59+
});
60+
61+
// 2. Load Item (Network Request -> Mocked Response)
62+
console.log('\nLoading Customer Object...');
63+
const customer = await manager.load('objects', 'customer');
64+
console.log('Result:', customer ? customer.label : 'Not Found');
65+
66+
// 3. Load Missing Item
67+
console.log('\nLoading Missing Object...');
68+
const missing = await manager.load('objects', 'ghost');
69+
console.log('Result:', missing ? 'Found' : 'Correctly returned null');
70+
71+
console.log('\n--- Demo Complete ---');
72+
}
73+
74+
runRemoteDemo().catch(console.error);

packages/client/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,21 @@ export class ObjectStackClient {
161161
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`);
162162
return res.json();
163163
},
164+
165+
/**
166+
* Save a metadata item
167+
* @param type - Metadata type (e.g., 'object', 'plugin')
168+
* @param name - Item name
169+
* @param item - The metadata content to save
170+
*/
171+
saveItem: async (type: string, name: string, item: any) => {
172+
const route = this.getRoute('metadata');
173+
const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`, {
174+
method: 'PUT',
175+
body: JSON.stringify(item)
176+
});
177+
return res.json();
178+
},
164179

165180
/**
166181
* Get object metadata with cache support

packages/metadata/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
"description": "Metadata loading, saving, and persistence for ObjectStack",
66
"main": "src/index.ts",
77
"types": "src/index.ts",
8+
"exports": {
9+
".": {
10+
"types": "./src/index.ts",
11+
"import": "./src/index.ts",
12+
"default": "./src/index.ts"
13+
},
14+
"./node": {
15+
"types": "./src/node.ts",
16+
"import": "./src/node.ts",
17+
"default": "./src/node.ts"
18+
}
19+
},
820
"scripts": {
921
"build": "tsc",
1022
"dev": "tsc --watch",

packages/metadata/src/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
*/
66

77
// Main Manager
8-
export { MetadataManager, type WatchCallback } from './metadata-manager.js';
9-
10-
// Plugin
11-
export { MetadataPlugin } from './plugin.js';
8+
export { MetadataManager, type WatchCallback, type MetadataManagerOptions } from './metadata-manager.js';
129

1310
// Loaders
1411
export { type MetadataLoader } from './loaders/loader-interface.js';
15-
export { FilesystemLoader } from './loaders/filesystem-loader.js';
1612
export { MemoryLoader } from './loaders/memory-loader.js';
13+
export { RemoteLoader } from './loaders/remote-loader.js';
1714

1815
// Serializers
1916
export { type MetadataSerializer, type SerializeOptions } from './serializers/serializer-interface.js';

0 commit comments

Comments
 (0)