Skip to content

Commit 0c7eb7e

Browse files
committed
feat: add MemoryLoader for in-memory metadata management; implement local and virtual mode demos with YAML and JSON support
1 parent 74210d0 commit 0c7eb7e

6 files changed

Lines changed: 261 additions & 0 deletions

File tree

content/docs/objectos/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"index",
55
"lifecycle",
66
"runtime-capabilities",
7+
"metadata-service",
78
"plugin-spec",
89
"config-resolution",
910
"i18n-standard",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: all_customers
2+
label: All Customers
3+
type: view
4+
object: customer
5+
filter:
6+
- - status
7+
- '='
8+
- active
9+
columns:
10+
- name
11+
- company
12+
- email
13+
- phone
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { MetadataManager } from '@objectstack/metadata';
2+
import * as path from 'node:path';
3+
import * as fs from 'node:fs/promises';
4+
5+
// 模拟用户的项目根目录
6+
const PROJECT_ROOT = path.resolve(process.cwd(), 'examples/metadata-demo/my-app');
7+
8+
async function main() {
9+
console.log('🏁 启动本地文件系统模式演示');
10+
console.log(`📂 项目目录: ${PROJECT_ROOT}`);
11+
12+
// 1. 初始化管理器,指向项目根目录
13+
// MetadataManager 会自动在这个目录下寻找 `objects`, `views`, `apps` 等子目录
14+
const manager = new MetadataManager({
15+
rootDir: path.join(PROJECT_ROOT, 'src'), // 假设元数据在 src 下
16+
formats: ['yaml', 'json'], // 优先使用 YAML
17+
watch: true // 开启监听
18+
});
19+
20+
// 2. 模拟用户在 UI 上创建了一个新的 "客户列表视图"
21+
const newView = {
22+
name: 'all_customers',
23+
label: 'All Customers',
24+
type: 'view',
25+
object: 'customer',
26+
filter: [['status', '=', 'active']],
27+
columns: ['name', 'company', 'email', 'phone']
28+
};
29+
30+
console.log('\n💾 正在保存视图 "all_customers"...');
31+
32+
// 3. 调用 save 方法
33+
// 不需要指定路径,Manager 会根据 type='view' 自动路由到 `src/views/` 目录
34+
const result = await manager.save('view', 'all_customers', newView, {
35+
format: 'yaml', // 强制保存为 YAML,更易读
36+
create: true
37+
});
38+
39+
if (result.success) {
40+
console.log(`✅ 保存成功!`);
41+
console.log(`📍 文件路径: ${result.path}`);
42+
43+
// 验证文件内容
44+
const content = await fs.readFile(result.path!, 'utf-8');
45+
console.log('\n📄 文件内容 preview:');
46+
console.log('-------------------');
47+
console.log(content);
48+
console.log('-------------------');
49+
}
50+
51+
// 4. 读取验证
52+
const loaded = await manager.load('view', 'all_customers');
53+
console.log(`\n🔍 从硬盘重新读取: [${loaded.label}] (过滤条件: ${loaded.filter})`);
54+
55+
// 清理演示文件
56+
await manager.stopWatching();
57+
}
58+
59+
main().catch(console.error);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { MetadataManager, MemoryLoader } from '@objectstack/metadata';
2+
import * as path from 'node:path';
3+
4+
// 模拟项目的元数据目录
5+
const PROJECT_ROOT = path.resolve(process.cwd(), 'examples/metadata-demo/metadata');
6+
7+
async function main() {
8+
console.log('🤖 启动内存虚拟模式演示 (Virtual Save)');
9+
console.log('💡 所有更改仅在内存中生效,重启服务或脚本即失效。\n');
10+
11+
// 1. 初始化管理器
12+
const manager = new MetadataManager({
13+
rootDir: PROJECT_ROOT,
14+
formats: ['json', 'yaml']
15+
});
16+
17+
// 2. 核心步骤:注册 MemoryLoader,并设置为"优先"
18+
// 注意:MetadataManager 遍历 loader 的顺序决定了读取优先级
19+
// 所以我们需要把 memory loader 放在前面,或者利用 save({ loader: 'memory' })
20+
21+
// 在当前实现中,manager.loaders 是 Map,保持插入顺序。
22+
// FilesystemLoader 在构造函数中已被添加。我们添加 MemoryLoader 到末尾。
23+
// 但是,我们可以手动指定 loader 来保存。
24+
25+
// 更好的方式:创建一个新的 manager实例,手动控制 loader 注册顺序(如果 MetadataManager API 支持清空或自定义的话)
26+
// 这里我们直接注册,并在保存时显式指定 'memory'。
27+
// 读取时,MetadataManager 会遍历所有 loader,如果 MemoryLoader 在 Map 中(即使在后),
28+
// 只要它的 `load` 方法返回数据,通常我们会接受。
29+
// *注意*:当前 MetadataManager.load 实现是 "iterate and return first found"。
30+
// 如果 Filesystem 里有同名文件,它可能会因为 FilesystemLoader 先注册而被先读取。
31+
// 为了实现 "Memory Override" (Shadowing),我们需要 MemoryLoader 排在 FilesystemLoader 之前。
32+
33+
// HACK: 为了演示效果,我们重新注册 loaders 来调整顺序
34+
// (在实际生产代码中,应该在构造 MetadataManager 时通过配置传入 loaders 列表,目前 API 可能还不完全支持)
35+
// 我们手动创建一个 MemoryLoader
36+
const memoryLoader = new MemoryLoader();
37+
38+
// 我们可以通过这种方式 hack 顺序 (不推荐但在演示中有效,或者修改 MetadataManager 支持 prepend)
39+
// 这里我们演示 "显式指定 storage" 的方式
40+
manager.registerLoader(memoryLoader);
41+
42+
// 3. 读取一个已存在的文件系统对象
43+
console.log('📚 读取文件系统中的 "demo_object"...');
44+
const fsObject = await manager.load('object', 'demo_object');
45+
console.log(` 原始 Label: ${fsObject?.label}`);
46+
47+
// 4. 进行 "虚拟修改"
48+
if (fsObject) {
49+
console.log('\n✏️ 正在进行虚拟修改 (修改 Label 为 "VIRTUAL OBJECT")...');
50+
const virtualObject = { ...fsObject, label: 'VIRTUAL OBJECT (Memory Only)' };
51+
52+
// 保存到内存
53+
await manager.save('object', 'demo_object', virtualObject, {
54+
loader: 'memory' // <--- 关键:指定保存到内存
55+
});
56+
console.log(' ✅ 已保存到内存缓存');
57+
}
58+
59+
// 5. 验证:尝试读取
60+
// 注意:因为 MetadataManager 默认按注册顺序读取,Filesystem 先注册。
61+
// 如果我们想读取内存版本,要么指定 loader,要么 MemoryLoader 需要在 Filesystem 之前。
62+
// 当前 API load() 没有 loader 参数。
63+
// 让我们测试一下 load() 是否能读到。如果不能,说明需要调整 Loader 顺序策略。
64+
65+
console.log('\n🔍 尝试读取 "demo_object"...');
66+
console.log(' (注意:如果 MetadataManager 优先读取了文件系统,这里可能看不到变化)');
67+
const loaded = await manager.load('object', 'demo_object');
68+
console.log(` 当前 Label: ${loaded?.label}`);
69+
70+
// 6. 专门从内存读取验证
71+
const memLoader = (manager as any).loaders.get('memory');
72+
const memObj = await memLoader.load('object', 'demo_object');
73+
console.log(`\n🕵️ 直接从 MemoryLoader 读取:`);
74+
console.log(` Label: ${memObj.data?.label}`);
75+
76+
// 7. 新建一个只有内存里有的对象
77+
console.log('\n✨ 创建纯内存对象 "ghost_view"...');
78+
await manager.save('view', 'ghost_view', { label: 'Ghost View' }, { loader: 'memory' });
79+
80+
const ghostList = await manager.list('view');
81+
console.log(` 全局 List (View): ${ghostList.includes('ghost_view') ? '包含 ghost_view' : '未找到'}`);
82+
83+
console.log('\n🛑 演示结束。脚本退出后,这些内存数据将消失。');
84+
}
85+
86+
main().catch(console.error);

packages/metadata/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { MetadataPlugin } from './plugin.js';
1313
// Loaders
1414
export { type MetadataLoader } from './loaders/loader-interface.js';
1515
export { FilesystemLoader } from './loaders/filesystem-loader.js';
16+
export { MemoryLoader } from './loaders/memory-loader.js';
1617

1718
// Serializers
1819
export { type MetadataSerializer, type SerializeOptions } from './serializers/serializer-interface.js';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Memory Metadata Loader
3+
*
4+
* Stores metadata in memory only. Changes are lost when process restarts.
5+
* Useful for testing, temporary overrides, or "dirty" edits.
6+
*/
7+
8+
import type {
9+
MetadataLoadOptions,
10+
MetadataLoadResult,
11+
MetadataStats,
12+
MetadataLoaderContract,
13+
MetadataSaveOptions,
14+
MetadataSaveResult,
15+
} from '@objectstack/spec/system';
16+
import type { MetadataLoader } from './loader-interface.js';
17+
18+
export class MemoryLoader implements MetadataLoader {
19+
readonly contract: MetadataLoaderContract = {
20+
name: 'memory',
21+
protocol: 'memory',
22+
capabilities: {
23+
read: true,
24+
write: true,
25+
watch: false,
26+
list: true,
27+
},
28+
};
29+
30+
// Storage: Type -> Name -> Data
31+
private storage = new Map<string, Map<string, any>>();
32+
33+
async load(
34+
type: string,
35+
name: string,
36+
_options?: MetadataLoadOptions
37+
): Promise<MetadataLoadResult> {
38+
const typeStore = this.storage.get(type);
39+
const data = typeStore?.get(name);
40+
41+
if (data) {
42+
return {
43+
data,
44+
source: 'memory',
45+
format: 'json',
46+
loadTime: 0,
47+
};
48+
}
49+
50+
return { data: null };
51+
}
52+
53+
async loadMany<T = any>(
54+
type: string,
55+
_options?: MetadataLoadOptions
56+
): Promise<T[]> {
57+
const typeStore = this.storage.get(type);
58+
if (!typeStore) return [];
59+
return Array.from(typeStore.values()) as T[];
60+
}
61+
62+
async exists(type: string, name: string): Promise<boolean> {
63+
return this.storage.get(type)?.has(name) ?? false;
64+
}
65+
66+
async stat(type: string, name: string): Promise<MetadataStats | null> {
67+
if (await this.exists(type, name)) {
68+
return {
69+
size: 0, // In-memory
70+
mtime: new Date(),
71+
format: 'json',
72+
};
73+
}
74+
return null;
75+
}
76+
77+
async list(type: string): Promise<string[]> {
78+
const typeStore = this.storage.get(type);
79+
if (!typeStore) return [];
80+
return Array.from(typeStore.keys());
81+
}
82+
83+
async save(
84+
type: string,
85+
name: string,
86+
data: any,
87+
_options?: MetadataSaveOptions
88+
): Promise<MetadataSaveResult> {
89+
if (!this.storage.has(type)) {
90+
this.storage.set(type, new Map());
91+
}
92+
93+
this.storage.get(type)!.set(name, data);
94+
95+
return {
96+
success: true,
97+
path: `memory://${type}/${name}`,
98+
saveTime: 0,
99+
};
100+
}
101+
}

0 commit comments

Comments
 (0)