@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +7 -452
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -1
  6. package/dist/loaders/filesystem-loader.d.ts +2 -1
  7. package/dist/loaders/filesystem-loader.d.ts.map +1 -1
  8. package/dist/loaders/filesystem-loader.js +83 -1
  9. package/dist/loaders/loader-interface.d.ts +9 -1
  10. package/dist/loaders/loader-interface.d.ts.map +1 -1
  11. package/dist/loaders/memory-loader.d.ts +19 -0
  12. package/dist/loaders/memory-loader.d.ts.map +1 -0
  13. package/dist/loaders/memory-loader.js +71 -0
  14. package/dist/loaders/remote-loader.d.ts +22 -0
  15. package/dist/loaders/remote-loader.d.ts.map +1 -0
  16. package/dist/loaders/remote-loader.js +103 -0
  17. package/dist/metadata-manager.d.ts +25 -23
  18. package/dist/metadata-manager.d.ts.map +1 -1
  19. package/dist/metadata-manager.js +104 -156
  20. package/dist/node-metadata-manager.d.ts +26 -0
  21. package/dist/node-metadata-manager.d.ts.map +1 -0
  22. package/dist/node-metadata-manager.js +98 -0
  23. package/dist/node.d.ts +8 -0
  24. package/dist/node.d.ts.map +1 -0
  25. package/dist/node.js +7 -0
  26. package/dist/plugin.d.ts +2 -2
  27. package/dist/plugin.d.ts.map +1 -1
  28. package/dist/plugin.js +55 -55
  29. package/package.json +17 -4
  30. package/src/index.ts +3 -2
  31. package/src/loaders/filesystem-loader.ts +106 -2
  32. package/src/loaders/loader-interface.ts +17 -0
  33. package/src/loaders/memory-loader.ts +101 -0
  34. package/src/loaders/remote-loader.ts +138 -0
  35. package/src/metadata-manager.ts +121 -193
  36. package/src/node-metadata-manager.ts +124 -0
  37. package/src/node.ts +8 -0
  38. 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): Promise<void>;
13
- start(ctx: PluginContext): Promise<void>;
12
+ init: (ctx: PluginContext) => Promise<void>;
13
+ start: (ctx: PluginContext) => Promise<void>;
14
14
  }
15
15
  //# sourceMappingURL=plugin.d.ts.map
@@ -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,CAAkB;IACjC,OAAO,CAAC,OAAO,CAAwB;gBAE3B,OAAO,GAAE,qBAA0B;IAezC,IAAI,CAAC,GAAG,EAAE,aAAa;IAQvB,KAAK,CAAC,GAAG,EAAE,aAAa;CAiDjC"}
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 { MetadataManager } from './metadata-manager.js';
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 MetadataManager({
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.9.2",
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.9.2",
20
- "@objectstack/spec": "0.9.2",
21
- "@objectstack/types": "0.9.2"
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 { FilesystemLoader } from './loaders/filesystem-loader.js';
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
+ }