@objectstack/metadata 0.9.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +7 -452
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/loaders/filesystem-loader.d.ts +2 -1
- package/dist/loaders/filesystem-loader.d.ts.map +1 -1
- package/dist/loaders/filesystem-loader.js +83 -1
- package/dist/loaders/loader-interface.d.ts +9 -1
- package/dist/loaders/loader-interface.d.ts.map +1 -1
- package/dist/loaders/memory-loader.d.ts +19 -0
- package/dist/loaders/memory-loader.d.ts.map +1 -0
- package/dist/loaders/memory-loader.js +71 -0
- package/dist/loaders/remote-loader.d.ts +22 -0
- package/dist/loaders/remote-loader.d.ts.map +1 -0
- package/dist/loaders/remote-loader.js +103 -0
- package/dist/metadata-manager.d.ts +25 -23
- package/dist/metadata-manager.d.ts.map +1 -1
- package/dist/metadata-manager.js +104 -156
- package/dist/node-metadata-manager.d.ts +26 -0
- package/dist/node-metadata-manager.d.ts.map +1 -0
- package/dist/node-metadata-manager.js +98 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +7 -0
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +55 -55
- package/package.json +17 -4
- package/src/index.ts +3 -2
- package/src/loaders/filesystem-loader.ts +106 -2
- package/src/loaders/loader-interface.ts +17 -0
- package/src/loaders/memory-loader.ts +101 -0
- package/src/loaders/remote-loader.ts +138 -0
- package/src/metadata-manager.ts +121 -193
- package/src/node-metadata-manager.ts +124 -0
- package/src/node.ts +8 -0
- package/src/plugin.ts +5 -5
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
import type { MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataLoaderContract, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
8
|
+
import type { MetadataLoader } from './loader-interface.js';
|
|
9
|
+
export declare class MemoryLoader implements MetadataLoader {
|
|
10
|
+
readonly contract: MetadataLoaderContract;
|
|
11
|
+
private storage;
|
|
12
|
+
load(type: string, name: string, _options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
|
|
13
|
+
loadMany<T = any>(type: string, _options?: MetadataLoadOptions): Promise<T[]>;
|
|
14
|
+
exists(type: string, name: string): Promise<boolean>;
|
|
15
|
+
stat(type: string, name: string): Promise<MetadataStats | null>;
|
|
16
|
+
list(type: string): Promise<string[]>;
|
|
17
|
+
save(type: string, name: string, data: any, _options?: MetadataSaveOptions): Promise<MetadataSaveResult>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=memory-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-loader.d.ts","sourceRoot":"","sources":["../../src/loaders/memory-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,qBAAa,YAAa,YAAW,cAAc;IACjD,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CASvC;IAGF,OAAO,CAAC,OAAO,CAAuC;IAEhD,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,kBAAkB,CAAC;IAgBxB,QAAQ,CAAC,CAAC,GAAG,GAAG,EACpB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,CAAC,EAAE,CAAC;IAMT,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAW/D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAMrC,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,kBAAkB,CAAC;CAa/B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
export class MemoryLoader {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.contract = {
|
|
10
|
+
name: 'memory',
|
|
11
|
+
protocol: 'memory',
|
|
12
|
+
capabilities: {
|
|
13
|
+
read: true,
|
|
14
|
+
write: true,
|
|
15
|
+
watch: false,
|
|
16
|
+
list: true,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
// Storage: Type -> Name -> Data
|
|
20
|
+
this.storage = new Map();
|
|
21
|
+
}
|
|
22
|
+
async load(type, name, _options) {
|
|
23
|
+
const typeStore = this.storage.get(type);
|
|
24
|
+
const data = typeStore?.get(name);
|
|
25
|
+
if (data) {
|
|
26
|
+
return {
|
|
27
|
+
data,
|
|
28
|
+
source: 'memory',
|
|
29
|
+
format: 'json',
|
|
30
|
+
loadTime: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { data: null };
|
|
34
|
+
}
|
|
35
|
+
async loadMany(type, _options) {
|
|
36
|
+
const typeStore = this.storage.get(type);
|
|
37
|
+
if (!typeStore)
|
|
38
|
+
return [];
|
|
39
|
+
return Array.from(typeStore.values());
|
|
40
|
+
}
|
|
41
|
+
async exists(type, name) {
|
|
42
|
+
return this.storage.get(type)?.has(name) ?? false;
|
|
43
|
+
}
|
|
44
|
+
async stat(type, name) {
|
|
45
|
+
if (await this.exists(type, name)) {
|
|
46
|
+
return {
|
|
47
|
+
size: 0, // In-memory
|
|
48
|
+
mtime: new Date(),
|
|
49
|
+
format: 'json',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async list(type) {
|
|
55
|
+
const typeStore = this.storage.get(type);
|
|
56
|
+
if (!typeStore)
|
|
57
|
+
return [];
|
|
58
|
+
return Array.from(typeStore.keys());
|
|
59
|
+
}
|
|
60
|
+
async save(type, name, data, _options) {
|
|
61
|
+
if (!this.storage.has(type)) {
|
|
62
|
+
this.storage.set(type, new Map());
|
|
63
|
+
}
|
|
64
|
+
this.storage.get(type).set(name, data);
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
path: `memory://${type}/${name}`,
|
|
68
|
+
saveTime: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Metadata Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads metadata from an HTTP API.
|
|
5
|
+
* This loader is stateless and delegates storage to the remote server.
|
|
6
|
+
*/
|
|
7
|
+
import type { MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataLoaderContract, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
8
|
+
import type { MetadataLoader } from './loader-interface.js';
|
|
9
|
+
export declare class RemoteLoader implements MetadataLoader {
|
|
10
|
+
private baseUrl;
|
|
11
|
+
private authToken?;
|
|
12
|
+
readonly contract: MetadataLoaderContract;
|
|
13
|
+
constructor(baseUrl: string, authToken?: string | undefined);
|
|
14
|
+
private get headers();
|
|
15
|
+
load(type: string, name: string, _options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
|
|
16
|
+
loadMany<T = any>(type: string, _options?: MetadataLoadOptions): Promise<T[]>;
|
|
17
|
+
exists(type: string, name: string): Promise<boolean>;
|
|
18
|
+
stat(type: string, name: string): Promise<MetadataStats | null>;
|
|
19
|
+
list(type: string): Promise<string[]>;
|
|
20
|
+
save(type: string, name: string, data: any, _options?: MetadataSaveOptions): Promise<MetadataSaveResult>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=remote-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-loader.d.ts","sourceRoot":"","sources":["../../src/loaders/remote-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,qBAAa,YAAa,YAAW,cAAc;IAYrC,OAAO,CAAC,OAAO;IAAU,OAAO,CAAC,SAAS,CAAC;IAXvD,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CASvC;gBAEkB,OAAO,EAAE,MAAM,EAAU,SAAS,CAAC,EAAE,MAAM,YAAA;IAE/D,OAAO,KAAK,OAAO,GAKlB;IAEK,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,kBAAkB,CAAC;IA4BxB,QAAQ,CAAC,CAAC,GAAG,GAAG,EACpB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,CAAC,EAAE,CAAC;IAaT,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQpD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAgB/D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKrC,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,kBAAkB,CAAC;CAiB/B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Metadata Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads metadata from an HTTP API.
|
|
5
|
+
* This loader is stateless and delegates storage to the remote server.
|
|
6
|
+
*/
|
|
7
|
+
export class RemoteLoader {
|
|
8
|
+
constructor(baseUrl, authToken) {
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
this.authToken = authToken;
|
|
11
|
+
this.contract = {
|
|
12
|
+
name: 'remote',
|
|
13
|
+
protocol: 'http',
|
|
14
|
+
capabilities: {
|
|
15
|
+
read: true,
|
|
16
|
+
write: true,
|
|
17
|
+
watch: false, // Could implement SSE/WebSocket in future
|
|
18
|
+
list: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
get headers() {
|
|
23
|
+
return {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...(this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async load(type, name, _options) {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: this.headers,
|
|
33
|
+
});
|
|
34
|
+
if (response.status === 404) {
|
|
35
|
+
return { data: null };
|
|
36
|
+
}
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Remote load failed: ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
return {
|
|
42
|
+
data,
|
|
43
|
+
source: this.baseUrl,
|
|
44
|
+
format: 'json',
|
|
45
|
+
loadTime: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`RemoteLoader error loading ${type}/${name}`, error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async loadMany(type, _options) {
|
|
54
|
+
const response = await fetch(`${this.baseUrl}/${type}`, {
|
|
55
|
+
method: 'GET',
|
|
56
|
+
headers: this.headers,
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return (await response.json());
|
|
62
|
+
}
|
|
63
|
+
async exists(type, name) {
|
|
64
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
65
|
+
method: 'HEAD',
|
|
66
|
+
headers: this.headers,
|
|
67
|
+
});
|
|
68
|
+
return response.ok;
|
|
69
|
+
}
|
|
70
|
+
async stat(type, name) {
|
|
71
|
+
// Basic implementation using HEAD
|
|
72
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
73
|
+
method: 'HEAD',
|
|
74
|
+
headers: this.headers,
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
size: Number(response.headers.get('content-length') || 0),
|
|
80
|
+
mtime: new Date(response.headers.get('last-modified') || Date.now()),
|
|
81
|
+
format: 'json',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async list(type) {
|
|
85
|
+
const items = await this.loadMany(type);
|
|
86
|
+
return items.map(i => i.name);
|
|
87
|
+
}
|
|
88
|
+
async save(type, name, data, _options) {
|
|
89
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
90
|
+
method: 'PUT',
|
|
91
|
+
headers: this.headers,
|
|
92
|
+
body: JSON.stringify(data),
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Remote save failed: ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
path: `${this.baseUrl}/${type}/${name}`,
|
|
100
|
+
saveTime: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Metadata Manager
|
|
3
3
|
*
|
|
4
|
-
* Main orchestrator for metadata loading, saving, and persistence
|
|
4
|
+
* Main orchestrator for metadata loading, saving, and persistence.
|
|
5
|
+
* Browser-compatible (Pure).
|
|
5
6
|
*/
|
|
6
|
-
import type { MetadataManagerConfig, MetadataLoadOptions, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent } from '@objectstack/spec/system';
|
|
7
|
+
import type { MetadataManagerConfig, MetadataLoadOptions, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent, MetadataFormat } from '@objectstack/spec/system';
|
|
8
|
+
import { type Logger } from '@objectstack/core';
|
|
9
|
+
import type { MetadataSerializer } from './serializers/serializer-interface.js';
|
|
10
|
+
import type { MetadataLoader } from './loaders/loader-interface.js';
|
|
7
11
|
/**
|
|
8
12
|
* Watch callback function
|
|
9
13
|
*/
|
|
10
14
|
export type WatchCallback = (event: MetadataWatchEvent) => void | Promise<void>;
|
|
15
|
+
export interface MetadataManagerOptions extends MetadataManagerConfig {
|
|
16
|
+
loaders?: MetadataLoader[];
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
19
|
* Main metadata manager class
|
|
13
20
|
*/
|
|
14
21
|
export declare class MetadataManager {
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
private loaders;
|
|
23
|
+
protected serializers: Map<MetadataFormat, MetadataSerializer>;
|
|
24
|
+
protected logger: Logger;
|
|
25
|
+
protected watchCallbacks: Map<string, Set<WatchCallback>>;
|
|
26
|
+
protected config: MetadataManagerOptions;
|
|
27
|
+
constructor(config: MetadataManagerOptions);
|
|
28
|
+
/**
|
|
29
|
+
* Register a new metadata loader (data source)
|
|
30
|
+
*/
|
|
31
|
+
registerLoader(loader: MetadataLoader): void;
|
|
22
32
|
/**
|
|
23
33
|
* Load a single metadata item
|
|
34
|
+
* Iterates through registered loaders until found
|
|
24
35
|
*/
|
|
25
36
|
load<T = any>(type: string, name: string, options?: MetadataLoadOptions): Promise<T | null>;
|
|
26
37
|
/**
|
|
27
38
|
* Load multiple metadata items
|
|
39
|
+
* Aggregates results from all loaders
|
|
28
40
|
*/
|
|
29
41
|
loadMany<T = any>(type: string, options?: MetadataLoadOptions): Promise<T[]>;
|
|
30
42
|
/**
|
|
31
43
|
* Save metadata to disk
|
|
32
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* Save metadata item
|
|
47
|
+
*/
|
|
33
48
|
save<T = any>(type: string, name: string, data: T, options?: MetadataSaveOptions): Promise<MetadataSaveResult>;
|
|
34
49
|
/**
|
|
35
50
|
* Check if metadata item exists
|
|
@@ -51,19 +66,6 @@ export declare class MetadataManager {
|
|
|
51
66
|
* Stop all watching
|
|
52
67
|
*/
|
|
53
68
|
stopWatching(): Promise<void>;
|
|
54
|
-
|
|
55
|
-
* Start watching for file changes
|
|
56
|
-
*/
|
|
57
|
-
private startWatching;
|
|
58
|
-
/**
|
|
59
|
-
* Handle file change events
|
|
60
|
-
*/
|
|
61
|
-
private handleFileEvent;
|
|
62
|
-
/**
|
|
63
|
-
* Generate ETag for content
|
|
64
|
-
* Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
|
|
65
|
-
* while keeping ETag headers compact (full 64-char hash is overkill for this use case)
|
|
66
|
-
*/
|
|
67
|
-
private generateETag;
|
|
69
|
+
protected notifyWatchers(type: string, event: MetadataWatchEvent): void;
|
|
68
70
|
}
|
|
69
71
|
//# sourceMappingURL=metadata-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-manager.d.ts","sourceRoot":"","sources":["../src/metadata-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"metadata-manager.d.ts","sourceRoot":"","sources":["../src/metadata-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAI9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEhF,MAAM,WAAW,sBAAuB,SAAQ,qBAAqB;IACnE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAA0C;IAEzD,SAAS,CAAC,WAAW,EAAE,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IAC/D,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,cAAc,kCAAyC;IACjE,SAAS,CAAC,MAAM,EAAE,sBAAsB,CAAC;gBAE7B,MAAM,EAAE,sBAAsB;IA4B1C;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc;IAKrC;;;OAGG;IACG,IAAI,CAAC,CAAC,GAAG,GAAG,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBpB;;;OAGG;IACG,QAAQ,CAAC,CAAC,GAAG,GAAG,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,CAAC,EAAE,CAAC;IAiBf;;OAEG;IACH;;OAEG;IACG,IAAI,CAAC,CAAC,GAAG,GAAG,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC;IA0D9B;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS1D;;OAEG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS3C;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IAOlD;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI;IAUpD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB;CAejE"}
|
package/dist/metadata-manager.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Metadata Manager
|
|
3
3
|
*
|
|
4
|
-
* Main orchestrator for metadata loading, saving, and persistence
|
|
4
|
+
* Main orchestrator for metadata loading, saving, and persistence.
|
|
5
|
+
* Browser-compatible (Pure).
|
|
5
6
|
*/
|
|
6
|
-
import * as fs from 'node:fs/promises';
|
|
7
|
-
import * as path from 'node:path';
|
|
8
|
-
import { createHash } from 'node:crypto';
|
|
9
|
-
import { watch as chokidarWatch } from 'chokidar';
|
|
10
7
|
import { createLogger } from '@objectstack/core';
|
|
11
|
-
import { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
12
8
|
import { JSONSerializer } from './serializers/json-serializer.js';
|
|
13
9
|
import { YAMLSerializer } from './serializers/yaml-serializer.js';
|
|
14
10
|
import { TypeScriptSerializer } from './serializers/typescript-serializer.js';
|
|
@@ -17,8 +13,9 @@ import { TypeScriptSerializer } from './serializers/typescript-serializer.js';
|
|
|
17
13
|
*/
|
|
18
14
|
export class MetadataManager {
|
|
19
15
|
constructor(config) {
|
|
20
|
-
this.
|
|
16
|
+
this.loaders = new Map();
|
|
21
17
|
this.watchCallbacks = new Map();
|
|
18
|
+
this.config = config;
|
|
22
19
|
this.logger = createLogger({ level: 'info', format: 'pretty' });
|
|
23
20
|
// Initialize serializers
|
|
24
21
|
this.serializers = new Map();
|
|
@@ -35,117 +32,138 @@ export class MetadataManager {
|
|
|
35
32
|
if (formats.includes('javascript')) {
|
|
36
33
|
this.serializers.set('javascript', new TypeScriptSerializer('javascript'));
|
|
37
34
|
}
|
|
38
|
-
// Initialize
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Start watching if enabled
|
|
42
|
-
if (config.watch) {
|
|
43
|
-
this.startWatching();
|
|
35
|
+
// Initialize Loaders
|
|
36
|
+
if (config.loaders && config.loaders.length > 0) {
|
|
37
|
+
config.loaders.forEach(loader => this.registerLoader(loader));
|
|
44
38
|
}
|
|
39
|
+
// Note: No default loader in base class. Subclasses (NodeMetadataManager) or caller must provide one.
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register a new metadata loader (data source)
|
|
43
|
+
*/
|
|
44
|
+
registerLoader(loader) {
|
|
45
|
+
this.loaders.set(loader.contract.name, loader);
|
|
46
|
+
this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* Load a single metadata item
|
|
50
|
+
* Iterates through registered loaders until found
|
|
48
51
|
*/
|
|
49
52
|
async load(type, name, options) {
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// Priority: Database > Filesystem (Implementation-dependent)
|
|
54
|
+
// For now, we just iterate.
|
|
55
|
+
for (const loader of this.loaders.values()) {
|
|
56
|
+
try {
|
|
57
|
+
const result = await loader.load(type, name, options);
|
|
58
|
+
if (result.data) {
|
|
59
|
+
return result.data;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
52
67
|
}
|
|
53
68
|
/**
|
|
54
69
|
* Load multiple metadata items
|
|
70
|
+
* Aggregates results from all loaders
|
|
55
71
|
*/
|
|
56
72
|
async loadMany(type, options) {
|
|
57
|
-
|
|
73
|
+
const results = [];
|
|
74
|
+
for (const loader of this.loaders.values()) {
|
|
75
|
+
try {
|
|
76
|
+
const items = await loader.loadMany(type, options);
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
// TODO: Deduplicate based on 'name' if property exists
|
|
79
|
+
results.push(item);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
58
87
|
}
|
|
59
88
|
/**
|
|
60
89
|
* Save metadata to disk
|
|
61
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Save metadata item
|
|
93
|
+
*/
|
|
62
94
|
async save(type, name, data, options) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!
|
|
69
|
-
throw new Error(`
|
|
95
|
+
const targetLoader = options?.loader;
|
|
96
|
+
// Find suitable loader
|
|
97
|
+
let loader;
|
|
98
|
+
if (targetLoader) {
|
|
99
|
+
loader = this.loaders.get(targetLoader);
|
|
100
|
+
if (!loader) {
|
|
101
|
+
throw new Error(`Loader not found: ${targetLoader}`);
|
|
70
102
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// 1. Try to find existing writable loader containing this item (Update existing)
|
|
106
|
+
for (const l of this.loaders.values()) {
|
|
107
|
+
// Skip if loader is strictly read-only
|
|
108
|
+
if (!l.save)
|
|
109
|
+
continue;
|
|
77
110
|
try {
|
|
78
|
-
await
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// File doesn't exist, continue
|
|
83
|
-
if (error.code !== 'ENOENT') {
|
|
84
|
-
throw error;
|
|
111
|
+
if (await l.exists(type, name)) {
|
|
112
|
+
loader = l;
|
|
113
|
+
this.logger.info(`Updating existing metadata in loader: ${l.contract.name}`);
|
|
114
|
+
break;
|
|
85
115
|
}
|
|
86
116
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
90
|
-
// Create backup if requested
|
|
91
|
-
let backupPath;
|
|
92
|
-
if (backup) {
|
|
93
|
-
try {
|
|
94
|
-
await fs.access(filePath);
|
|
95
|
-
backupPath = `${filePath}.bak`;
|
|
96
|
-
await fs.copyFile(filePath, backupPath);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// File doesn't exist, no backup needed
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Ignore existence check errors (e.g. network down)
|
|
100
119
|
}
|
|
101
120
|
}
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Write to disk (atomic or direct)
|
|
109
|
-
if (atomic) {
|
|
110
|
-
const tempPath = `${filePath}.tmp`;
|
|
111
|
-
await fs.writeFile(tempPath, content, 'utf-8');
|
|
112
|
-
await fs.rename(tempPath, filePath);
|
|
121
|
+
// 2. Default to 'filesystem' if available (Create new)
|
|
122
|
+
if (!loader) {
|
|
123
|
+
const fsLoader = this.loaders.get('filesystem');
|
|
124
|
+
if (fsLoader && fsLoader.save) {
|
|
125
|
+
loader = fsLoader;
|
|
126
|
+
}
|
|
113
127
|
}
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
// 3. Fallback to any writable loader
|
|
129
|
+
if (!loader) {
|
|
130
|
+
for (const l of this.loaders.values()) {
|
|
131
|
+
if (l.save) {
|
|
132
|
+
loader = l;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
116
136
|
}
|
|
117
|
-
// Get stats
|
|
118
|
-
const stats = await fs.stat(filePath);
|
|
119
|
-
const etag = this.generateETag(content);
|
|
120
|
-
return {
|
|
121
|
-
success: true,
|
|
122
|
-
path: filePath,
|
|
123
|
-
etag,
|
|
124
|
-
size: stats.size,
|
|
125
|
-
saveTime: Date.now() - startTime,
|
|
126
|
-
backupPath,
|
|
127
|
-
};
|
|
128
137
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
throw error;
|
|
138
|
+
if (!loader) {
|
|
139
|
+
throw new Error(`No loader available for saving type: ${type}`);
|
|
140
|
+
}
|
|
141
|
+
if (!loader.save) {
|
|
142
|
+
throw new Error(`Loader '${loader.contract?.name}' does not support saving`);
|
|
136
143
|
}
|
|
144
|
+
return loader.save(type, name, data, options);
|
|
137
145
|
}
|
|
138
146
|
/**
|
|
139
147
|
* Check if metadata item exists
|
|
140
148
|
*/
|
|
141
149
|
async exists(type, name) {
|
|
142
|
-
|
|
150
|
+
for (const loader of this.loaders.values()) {
|
|
151
|
+
if (await loader.exists(type, name)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
143
156
|
}
|
|
144
157
|
/**
|
|
145
158
|
* List all items of a type
|
|
146
159
|
*/
|
|
147
160
|
async list(type) {
|
|
148
|
-
|
|
161
|
+
const items = new Set();
|
|
162
|
+
for (const loader of this.loaders.values()) {
|
|
163
|
+
const result = await loader.list(type);
|
|
164
|
+
result.forEach(item => items.add(item));
|
|
165
|
+
}
|
|
166
|
+
return Array.from(items);
|
|
149
167
|
}
|
|
150
168
|
/**
|
|
151
169
|
* Watch for metadata changes
|
|
@@ -172,92 +190,22 @@ export class MetadataManager {
|
|
|
172
190
|
* Stop all watching
|
|
173
191
|
*/
|
|
174
192
|
async stopWatching() {
|
|
175
|
-
|
|
176
|
-
await this.watcher.close();
|
|
177
|
-
this.watcher = undefined;
|
|
178
|
-
this.watchCallbacks.clear();
|
|
179
|
-
}
|
|
193
|
+
// Override in subclass
|
|
180
194
|
}
|
|
181
|
-
|
|
182
|
-
* Start watching for file changes
|
|
183
|
-
*/
|
|
184
|
-
startWatching() {
|
|
185
|
-
const rootDir = this.config.rootDir || process.cwd();
|
|
186
|
-
const { ignored = ['**/node_modules/**', '**/*.test.*'], persistent = true } = this.config.watchOptions || {};
|
|
187
|
-
this.watcher = chokidarWatch(rootDir, {
|
|
188
|
-
ignored,
|
|
189
|
-
persistent,
|
|
190
|
-
ignoreInitial: true,
|
|
191
|
-
});
|
|
192
|
-
this.watcher.on('add', async (filePath) => {
|
|
193
|
-
await this.handleFileEvent('added', filePath);
|
|
194
|
-
});
|
|
195
|
-
this.watcher.on('change', async (filePath) => {
|
|
196
|
-
await this.handleFileEvent('changed', filePath);
|
|
197
|
-
});
|
|
198
|
-
this.watcher.on('unlink', async (filePath) => {
|
|
199
|
-
await this.handleFileEvent('deleted', filePath);
|
|
200
|
-
});
|
|
201
|
-
this.logger.info('File watcher started', { rootDir });
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Handle file change events
|
|
205
|
-
*/
|
|
206
|
-
async handleFileEvent(eventType, filePath) {
|
|
207
|
-
const rootDir = this.config.rootDir || process.cwd();
|
|
208
|
-
const relativePath = path.relative(rootDir, filePath);
|
|
209
|
-
const parts = relativePath.split(path.sep);
|
|
210
|
-
if (parts.length < 2) {
|
|
211
|
-
return; // Not a metadata file
|
|
212
|
-
}
|
|
213
|
-
const type = parts[0];
|
|
214
|
-
const fileName = parts[parts.length - 1];
|
|
215
|
-
const name = path.basename(fileName, path.extname(fileName));
|
|
195
|
+
notifyWatchers(type, event) {
|
|
216
196
|
const callbacks = this.watchCallbacks.get(type);
|
|
217
|
-
if (!callbacks
|
|
197
|
+
if (!callbacks)
|
|
218
198
|
return;
|
|
219
|
-
}
|
|
220
|
-
let data = undefined;
|
|
221
|
-
if (eventType !== 'deleted') {
|
|
222
|
-
try {
|
|
223
|
-
data = await this.load(type, name, { useCache: false });
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
this.logger.error('Failed to load changed file', undefined, {
|
|
227
|
-
filePath,
|
|
228
|
-
error: error instanceof Error ? error.message : String(error),
|
|
229
|
-
});
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const event = {
|
|
234
|
-
type: eventType,
|
|
235
|
-
metadataType: type,
|
|
236
|
-
name,
|
|
237
|
-
path: filePath,
|
|
238
|
-
data,
|
|
239
|
-
timestamp: new Date(),
|
|
240
|
-
};
|
|
241
199
|
for (const callback of callbacks) {
|
|
242
200
|
try {
|
|
243
|
-
|
|
201
|
+
void callback(event);
|
|
244
202
|
}
|
|
245
203
|
catch (error) {
|
|
246
204
|
this.logger.error('Watch callback error', undefined, {
|
|
247
205
|
type,
|
|
248
|
-
name,
|
|
249
206
|
error: error instanceof Error ? error.message : String(error),
|
|
250
207
|
});
|
|
251
208
|
}
|
|
252
209
|
}
|
|
253
210
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Generate ETag for content
|
|
256
|
-
* Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
|
|
257
|
-
* while keeping ETag headers compact (full 64-char hash is overkill for this use case)
|
|
258
|
-
*/
|
|
259
|
-
generateETag(content) {
|
|
260
|
-
const hash = createHash('sha256').update(content).digest('hex').substring(0, 32);
|
|
261
|
-
return `"${hash}"`;
|
|
262
|
-
}
|
|
263
211
|
}
|