@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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Metadata Manager
|
|
3
|
+
*
|
|
4
|
+
* Extends MetadataManager with Filesystem capabilities (Watching, default loader)
|
|
5
|
+
*/
|
|
6
|
+
import { MetadataManager, type MetadataManagerOptions } from './metadata-manager.js';
|
|
7
|
+
/**
|
|
8
|
+
* Node metadata manager class
|
|
9
|
+
*/
|
|
10
|
+
export declare class NodeMetadataManager extends MetadataManager {
|
|
11
|
+
private watcher?;
|
|
12
|
+
constructor(config: MetadataManagerOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Stop all watching
|
|
15
|
+
*/
|
|
16
|
+
stopWatching(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Start watching for file changes
|
|
19
|
+
*/
|
|
20
|
+
private startWatching;
|
|
21
|
+
/**
|
|
22
|
+
* Handle file change events
|
|
23
|
+
*/
|
|
24
|
+
private handleFileEvent;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=node-metadata-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-metadata-manager.d.ts","sourceRoot":"","sources":["../src/node-metadata-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAErF;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,eAAe;IACtD,OAAO,CAAC,OAAO,CAAC,CAAY;gBAEhB,MAAM,EAAE,sBAAsB;IAgB1C;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAQnC;;OAEG;IACH,OAAO,CAAC,aAAa;IA0BrB;;OAEG;YACW,eAAe;CA4C9B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Metadata Manager
|
|
3
|
+
*
|
|
4
|
+
* Extends MetadataManager with Filesystem capabilities (Watching, default loader)
|
|
5
|
+
*/
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
8
|
+
import { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
9
|
+
import { MetadataManager } from './metadata-manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* Node metadata manager class
|
|
12
|
+
*/
|
|
13
|
+
export class NodeMetadataManager extends MetadataManager {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super(config);
|
|
16
|
+
// Initialize Default Filesystem Loader if no loaders provided
|
|
17
|
+
// This logic replaces the removed logic from base class
|
|
18
|
+
if (!config.loaders || config.loaders.length === 0) {
|
|
19
|
+
const rootDir = config.rootDir || process.cwd();
|
|
20
|
+
this.registerLoader(new FilesystemLoader(rootDir, this.serializers, this.logger));
|
|
21
|
+
}
|
|
22
|
+
// Start watching if enabled
|
|
23
|
+
if (config.watch) {
|
|
24
|
+
this.startWatching();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Stop all watching
|
|
29
|
+
*/
|
|
30
|
+
async stopWatching() {
|
|
31
|
+
if (this.watcher) {
|
|
32
|
+
await this.watcher.close();
|
|
33
|
+
this.watcher = undefined;
|
|
34
|
+
}
|
|
35
|
+
// Call base cleanup if any
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Start watching for file changes
|
|
39
|
+
*/
|
|
40
|
+
startWatching() {
|
|
41
|
+
const rootDir = this.config.rootDir || process.cwd();
|
|
42
|
+
const { ignored = ['**/node_modules/**', '**/*.test.*'], persistent = true } = this.config.watchOptions || {};
|
|
43
|
+
this.watcher = chokidarWatch(rootDir, {
|
|
44
|
+
ignored,
|
|
45
|
+
persistent,
|
|
46
|
+
ignoreInitial: true,
|
|
47
|
+
});
|
|
48
|
+
this.watcher.on('add', async (filePath) => {
|
|
49
|
+
await this.handleFileEvent('added', filePath);
|
|
50
|
+
});
|
|
51
|
+
this.watcher.on('change', async (filePath) => {
|
|
52
|
+
await this.handleFileEvent('changed', filePath);
|
|
53
|
+
});
|
|
54
|
+
this.watcher.on('unlink', async (filePath) => {
|
|
55
|
+
await this.handleFileEvent('deleted', filePath);
|
|
56
|
+
});
|
|
57
|
+
this.logger.info('File watcher started', { rootDir });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Handle file change events
|
|
61
|
+
*/
|
|
62
|
+
async handleFileEvent(eventType, filePath) {
|
|
63
|
+
const rootDir = this.config.rootDir || process.cwd();
|
|
64
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
65
|
+
const parts = relativePath.split(path.sep);
|
|
66
|
+
if (parts.length < 2) {
|
|
67
|
+
return; // Not a metadata file
|
|
68
|
+
}
|
|
69
|
+
const type = parts[0];
|
|
70
|
+
const fileName = parts[parts.length - 1];
|
|
71
|
+
const name = path.basename(fileName, path.extname(fileName));
|
|
72
|
+
// We can't access private watchCallbacks from parent.
|
|
73
|
+
// We need a protected method to trigger watch event or access it.
|
|
74
|
+
// OPTION: Add a method `triggerWatchEvent` to MetadataManager
|
|
75
|
+
let data = undefined;
|
|
76
|
+
if (eventType !== 'deleted') {
|
|
77
|
+
try {
|
|
78
|
+
data = await this.load(type, name, { useCache: false });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.logger.error('Failed to load changed file', undefined, {
|
|
82
|
+
filePath,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const event = {
|
|
89
|
+
type: eventType,
|
|
90
|
+
metadataType: type,
|
|
91
|
+
name,
|
|
92
|
+
path: filePath,
|
|
93
|
+
data,
|
|
94
|
+
timestamp: new Date(),
|
|
95
|
+
};
|
|
96
|
+
this.notifyWatchers(type, event);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js specific exports for @objectstack/metadata
|
|
3
|
+
*/
|
|
4
|
+
export * from './index.js';
|
|
5
|
+
export { NodeMetadataManager } from './node-metadata-manager.js';
|
|
6
|
+
export { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
7
|
+
export { MetadataPlugin } from './plugin.js';
|
|
8
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js specific exports for @objectstack/metadata
|
|
3
|
+
*/
|
|
4
|
+
export * from './index.js';
|
|
5
|
+
export { NodeMetadataManager } from './node-metadata-manager.js';
|
|
6
|
+
export { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
7
|
+
export { MetadataPlugin } from './plugin.js';
|
package/dist/plugin.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare class MetadataPlugin implements Plugin {
|
|
|
9
9
|
private manager;
|
|
10
10
|
private options;
|
|
11
11
|
constructor(options?: MetadataPluginOptions);
|
|
12
|
-
init(ctx: PluginContext)
|
|
13
|
-
start(ctx: PluginContext)
|
|
12
|
+
init: (ctx: PluginContext) => Promise<void>;
|
|
13
|
+
start: (ctx: PluginContext) => Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI1D,MAAM,WAAW,qBAAqB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,cAAe,YAAW,MAAM;IACzC,IAAI,SAA8B;IAClC,OAAO,SAAW;IAElB,OAAO,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI1D,MAAM,WAAW,qBAAqB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,cAAe,YAAW,MAAM;IACzC,IAAI,SAA8B;IAClC,OAAO,SAAW;IAElB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,OAAO,CAAwB;gBAE3B,OAAO,GAAE,qBAA0B;IAe/C,IAAI,GAAU,KAAK,aAAa,mBAM/B;IAED,KAAK,GAAU,KAAK,aAAa,mBAgDhC;CACJ"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NodeMetadataManager } from './node-metadata-manager.js';
|
|
2
2
|
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
|
|
3
3
|
export class MetadataPlugin {
|
|
4
4
|
constructor(options = {}) {
|
|
5
5
|
this.name = 'com.objectstack.metadata';
|
|
6
6
|
this.version = '1.0.0';
|
|
7
|
+
this.init = async (ctx) => {
|
|
8
|
+
ctx.logger.info('Initializing Metadata Manager', { root: this.options.rootDir || process.cwd() });
|
|
9
|
+
// Register Metadata Manager as a service
|
|
10
|
+
// This allows other plugins to query raw metadata or listen to changes
|
|
11
|
+
ctx.registerService('metadata', this.manager);
|
|
12
|
+
};
|
|
13
|
+
this.start = async (ctx) => {
|
|
14
|
+
ctx.logger.info('Loading metadata...');
|
|
15
|
+
// Define metadata types directly from the Protocol Definition
|
|
16
|
+
// This ensures the loader is always in sync with the Spec
|
|
17
|
+
const metadataTypes = Object.keys(ObjectStackDefinitionSchema.shape)
|
|
18
|
+
.filter(key => key !== 'manifest'); // Manifest is handled separately
|
|
19
|
+
for (const type of metadataTypes) {
|
|
20
|
+
try {
|
|
21
|
+
// Try to load metadata of this type
|
|
22
|
+
const items = await this.manager.loadMany(type, {
|
|
23
|
+
recursive: true
|
|
24
|
+
});
|
|
25
|
+
if (items.length > 0) {
|
|
26
|
+
ctx.logger.info(`Loaded ${items.length} ${type}`);
|
|
27
|
+
// Helper: Register with ObjectQL Registry
|
|
28
|
+
const ql = ctx.getService('objectql');
|
|
29
|
+
if (ql && ql.registry) {
|
|
30
|
+
items.forEach((item) => {
|
|
31
|
+
// Determine key field (id or name)
|
|
32
|
+
const keyField = item.id ? 'id' : 'name';
|
|
33
|
+
// Map plural type to singular/registry type if needed
|
|
34
|
+
// For now, we use the singular form for standard types:
|
|
35
|
+
// objects -> object, apps -> app, etc.
|
|
36
|
+
// But Registry seems to accept arbitrary strings.
|
|
37
|
+
// To match Protocol standard, we might want to normalize.
|
|
38
|
+
// Let's use the directory name (plural) as the type for now,
|
|
39
|
+
// OR map 'objects' -> 'object' specifically.
|
|
40
|
+
let registryType = type;
|
|
41
|
+
if (type === 'objects')
|
|
42
|
+
registryType = 'object';
|
|
43
|
+
if (type === 'apps')
|
|
44
|
+
registryType = 'app';
|
|
45
|
+
if (type === 'plugins')
|
|
46
|
+
registryType = 'plugin';
|
|
47
|
+
if (type === 'functions')
|
|
48
|
+
registryType = 'function';
|
|
49
|
+
ql.registry.registerItem(registryType, item, keyField);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
// Ignore missing directories or errors
|
|
56
|
+
// ctx.logger.debug(`No metadata found for type: ${type}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
7
60
|
this.options = {
|
|
8
61
|
watch: true,
|
|
9
62
|
...options
|
|
10
63
|
};
|
|
11
64
|
const rootDir = this.options.rootDir || process.cwd();
|
|
12
|
-
this.manager = new
|
|
65
|
+
this.manager = new NodeMetadataManager({
|
|
13
66
|
rootDir,
|
|
14
67
|
watch: this.options.watch ?? true,
|
|
15
68
|
formats: ['yaml', 'json', 'typescript', 'javascript']
|
|
16
69
|
});
|
|
17
70
|
}
|
|
18
|
-
async init(ctx) {
|
|
19
|
-
ctx.logger.info('Initializing Metadata Manager', { root: this.options.rootDir || process.cwd() });
|
|
20
|
-
// Register Metadata Manager as a service
|
|
21
|
-
// This allows other plugins to query raw metadata or listen to changes
|
|
22
|
-
ctx.registerService('metadata', this.manager);
|
|
23
|
-
}
|
|
24
|
-
async start(ctx) {
|
|
25
|
-
ctx.logger.info('Loading metadata...');
|
|
26
|
-
// Define metadata types directly from the Protocol Definition
|
|
27
|
-
// This ensures the loader is always in sync with the Spec
|
|
28
|
-
const metadataTypes = Object.keys(ObjectStackDefinitionSchema.shape)
|
|
29
|
-
.filter(key => key !== 'manifest'); // Manifest is handled separately
|
|
30
|
-
for (const type of metadataTypes) {
|
|
31
|
-
try {
|
|
32
|
-
// Try to load metadata of this type
|
|
33
|
-
const items = await this.manager.loadMany(type, {
|
|
34
|
-
recursive: true
|
|
35
|
-
});
|
|
36
|
-
if (items.length > 0) {
|
|
37
|
-
ctx.logger.info(`Loaded ${items.length} ${type}`);
|
|
38
|
-
// Helper: Register with ObjectQL Registry
|
|
39
|
-
const ql = ctx.getService('objectql');
|
|
40
|
-
if (ql && ql.registry) {
|
|
41
|
-
items.forEach((item) => {
|
|
42
|
-
// Determine key field (id or name)
|
|
43
|
-
const keyField = item.id ? 'id' : 'name';
|
|
44
|
-
// Map plural type to singular/registry type if needed
|
|
45
|
-
// For now, we use the singular form for standard types:
|
|
46
|
-
// objects -> object, apps -> app, etc.
|
|
47
|
-
// But Registry seems to accept arbitrary strings.
|
|
48
|
-
// To match Protocol standard, we might want to normalize.
|
|
49
|
-
// Let's use the directory name (plural) as the type for now,
|
|
50
|
-
// OR map 'objects' -> 'object' specifically.
|
|
51
|
-
let registryType = type;
|
|
52
|
-
if (type === 'objects')
|
|
53
|
-
registryType = 'object';
|
|
54
|
-
if (type === 'apps')
|
|
55
|
-
registryType = 'app';
|
|
56
|
-
if (type === 'plugins')
|
|
57
|
-
registryType = 'plugin';
|
|
58
|
-
if (type === 'functions')
|
|
59
|
-
registryType = 'function';
|
|
60
|
-
ql.registry.registerItem(registryType, item, keyField);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch (e) {
|
|
66
|
-
// Ignore missing directories or errors
|
|
67
|
-
// ctx.logger.debug(`No metadata found for type: ${type}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
71
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/metadata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"description": "Metadata loading, saving, and persistence for ObjectStack",
|
|
5
6
|
"main": "src/index.ts",
|
|
6
7
|
"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
|
+
},
|
|
7
20
|
"keywords": [
|
|
8
21
|
"objectstack",
|
|
9
22
|
"metadata",
|
|
@@ -16,9 +29,9 @@
|
|
|
16
29
|
"js-yaml": "^4.1.0",
|
|
17
30
|
"chokidar": "^3.5.3",
|
|
18
31
|
"zod": "^4.3.6",
|
|
19
|
-
"@objectstack/core": "0.
|
|
20
|
-
"@objectstack/spec": "0.
|
|
21
|
-
"@objectstack/types": "0.
|
|
32
|
+
"@objectstack/core": "1.0.1",
|
|
33
|
+
"@objectstack/spec": "1.0.1",
|
|
34
|
+
"@objectstack/types": "1.0.1"
|
|
22
35
|
},
|
|
23
36
|
"devDependencies": {
|
|
24
37
|
"@types/js-yaml": "^4.0.9",
|
package/src/index.ts
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main Manager
|
|
8
|
-
export { MetadataManager, type WatchCallback } from './metadata-manager.js';
|
|
8
|
+
export { MetadataManager, type WatchCallback, type MetadataManagerOptions } from './metadata-manager.js';
|
|
9
9
|
|
|
10
10
|
// Plugin
|
|
11
11
|
export { MetadataPlugin } from './plugin.js';
|
|
12
12
|
|
|
13
13
|
// Loaders
|
|
14
14
|
export { type MetadataLoader } from './loaders/loader-interface.js';
|
|
15
|
-
export {
|
|
15
|
+
export { MemoryLoader } from './loaders/memory-loader.js';
|
|
16
|
+
export { RemoteLoader } from './loaders/remote-loader.js';
|
|
16
17
|
|
|
17
18
|
// Serializers
|
|
18
19
|
export { type MetadataSerializer, type SerializeOptions } from './serializers/serializer-interface.js';
|
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
MetadataStats,
|
|
15
15
|
MetadataLoaderContract,
|
|
16
16
|
MetadataFormat,
|
|
17
|
+
MetadataSaveOptions,
|
|
18
|
+
MetadataSaveResult,
|
|
17
19
|
} from '@objectstack/spec/system';
|
|
18
20
|
import type { Logger } from '@objectstack/core';
|
|
19
21
|
import type { MetadataLoader } from './loader-interface.js';
|
|
@@ -22,6 +24,13 @@ import type { MetadataSerializer } from '../serializers/serializer-interface.js'
|
|
|
22
24
|
export class FilesystemLoader implements MetadataLoader {
|
|
23
25
|
readonly contract: MetadataLoaderContract = {
|
|
24
26
|
name: 'filesystem',
|
|
27
|
+
protocol: 'file',
|
|
28
|
+
capabilities: {
|
|
29
|
+
read: true,
|
|
30
|
+
write: true,
|
|
31
|
+
watch: true,
|
|
32
|
+
list: true,
|
|
33
|
+
},
|
|
25
34
|
supportedFormats: ['json', 'yaml', 'typescript', 'javascript'],
|
|
26
35
|
supportsWatch: true,
|
|
27
36
|
supportsWrite: true,
|
|
@@ -99,7 +108,7 @@ export class FilesystemLoader implements MetadataLoader {
|
|
|
99
108
|
|
|
100
109
|
// Load and deserialize
|
|
101
110
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
102
|
-
const serializer = this.getSerializer(stats.format);
|
|
111
|
+
const serializer = this.getSerializer(stats.format!);
|
|
103
112
|
|
|
104
113
|
if (!serializer) {
|
|
105
114
|
throw new Error(`No serializer found for format: ${stats.format}`);
|
|
@@ -111,7 +120,7 @@ export class FilesystemLoader implements MetadataLoader {
|
|
|
111
120
|
if (useCache) {
|
|
112
121
|
this.cache.set(cacheKey, {
|
|
113
122
|
data,
|
|
114
|
-
etag: stats.etag,
|
|
123
|
+
etag: stats.etag || '',
|
|
115
124
|
timestamp: Date.now(),
|
|
116
125
|
});
|
|
117
126
|
}
|
|
@@ -253,6 +262,101 @@ export class FilesystemLoader implements MetadataLoader {
|
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
async save(
|
|
266
|
+
type: string,
|
|
267
|
+
name: string,
|
|
268
|
+
data: any,
|
|
269
|
+
options?: MetadataSaveOptions
|
|
270
|
+
): Promise<MetadataSaveResult> {
|
|
271
|
+
const startTime = Date.now();
|
|
272
|
+
const {
|
|
273
|
+
format = 'typescript',
|
|
274
|
+
prettify = true,
|
|
275
|
+
indent = 2,
|
|
276
|
+
sortKeys = false,
|
|
277
|
+
backup = false,
|
|
278
|
+
overwrite = true,
|
|
279
|
+
atomic = true,
|
|
280
|
+
path: customPath,
|
|
281
|
+
} = options || {};
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Get serializer
|
|
285
|
+
const serializer = this.getSerializer(format);
|
|
286
|
+
if (!serializer) {
|
|
287
|
+
throw new Error(`No serializer found for format: ${format}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Determine file path
|
|
291
|
+
const typeDir = path.join(this.rootDir, type);
|
|
292
|
+
const fileName = `${name}${serializer.getExtension()}`;
|
|
293
|
+
const filePath = customPath || path.join(typeDir, fileName);
|
|
294
|
+
|
|
295
|
+
// Check if file exists
|
|
296
|
+
if (!overwrite) {
|
|
297
|
+
try {
|
|
298
|
+
await fs.access(filePath);
|
|
299
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
// File doesn't exist, continue
|
|
302
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Create directory if it doesn't exist
|
|
309
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
310
|
+
|
|
311
|
+
// Create backup if requested
|
|
312
|
+
let backupPath: string | undefined;
|
|
313
|
+
if (backup) {
|
|
314
|
+
try {
|
|
315
|
+
await fs.access(filePath);
|
|
316
|
+
backupPath = `${filePath}.bak`;
|
|
317
|
+
await fs.copyFile(filePath, backupPath);
|
|
318
|
+
} catch {
|
|
319
|
+
// File doesn't exist, no backup needed
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Serialize data
|
|
324
|
+
const content = serializer.serialize(data, {
|
|
325
|
+
prettify,
|
|
326
|
+
indent,
|
|
327
|
+
sortKeys,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Write to disk (atomic or direct)
|
|
331
|
+
if (atomic) {
|
|
332
|
+
const tempPath = `${filePath}.tmp`;
|
|
333
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
334
|
+
await fs.rename(tempPath, filePath);
|
|
335
|
+
} else {
|
|
336
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Update cache logic if needed (e.g., invalidate or update)
|
|
340
|
+
// For now, we rely on the watcher to pick up changes
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
success: true,
|
|
344
|
+
path: filePath,
|
|
345
|
+
// format, // Not in schema
|
|
346
|
+
size: Buffer.byteLength(content, 'utf-8'),
|
|
347
|
+
backupPath,
|
|
348
|
+
saveTime: Date.now() - startTime,
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
this.logger?.error('Failed to save metadata', undefined, {
|
|
352
|
+
type,
|
|
353
|
+
name,
|
|
354
|
+
error: error instanceof Error ? error.message : String(error),
|
|
355
|
+
});
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
256
360
|
/**
|
|
257
361
|
* Find file for a given type and name
|
|
258
362
|
*/
|
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
MetadataLoadResult,
|
|
10
10
|
MetadataStats,
|
|
11
11
|
MetadataLoaderContract,
|
|
12
|
+
MetadataSaveOptions,
|
|
13
|
+
MetadataSaveResult,
|
|
12
14
|
} from '@objectstack/spec/system';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -67,4 +69,19 @@ export interface MetadataLoader {
|
|
|
67
69
|
* @returns Array of item names
|
|
68
70
|
*/
|
|
69
71
|
list(type: string): Promise<string[]>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save metadata item
|
|
75
|
+
* @param type The metadata type
|
|
76
|
+
* @param name The item name
|
|
77
|
+
* @param data The data to save
|
|
78
|
+
* @param options Save options
|
|
79
|
+
*/
|
|
80
|
+
save?(
|
|
81
|
+
type: string,
|
|
82
|
+
name: string,
|
|
83
|
+
data: any,
|
|
84
|
+
options?: MetadataSaveOptions
|
|
85
|
+
): Promise<MetadataSaveResult>;
|
|
70
86
|
}
|
|
87
|
+
|
|
@@ -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
|
+
}
|