@objectstack/metadata 3.3.0 → 4.0.0
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/dist/index.cjs +2197 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +42 -82
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +2201 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +65 -0
- package/dist/node.d.ts +65 -0
- package/dist/{index.mjs → node.js} +3 -1
- package/package.json +22 -17
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -504
- package/ROADMAP.md +0 -224
- package/src/index.ts +0 -68
- package/src/loaders/database-loader.test.ts +0 -559
- package/src/loaders/database-loader.ts +0 -352
- package/src/loaders/filesystem-loader.ts +0 -420
- package/src/loaders/loader-interface.ts +0 -89
- package/src/loaders/memory-loader.ts +0 -103
- package/src/loaders/remote-loader.ts +0 -140
- package/src/metadata-manager.ts +0 -1168
- package/src/metadata-service.test.ts +0 -965
- package/src/metadata.test.ts +0 -431
- package/src/migration/executor.ts +0 -54
- package/src/migration/index.ts +0 -3
- package/src/node-metadata-manager.ts +0 -126
- package/src/node.ts +0 -11
- package/src/objects/sys-metadata.object.ts +0 -188
- package/src/plugin.ts +0 -102
- package/src/serializers/json-serializer.ts +0 -73
- package/src/serializers/serializer-interface.ts +0 -65
- package/src/serializers/serializers.test.ts +0 -74
- package/src/serializers/typescript-serializer.ts +0 -127
- package/src/serializers/yaml-serializer.ts +0 -49
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -23
- /package/dist/{index.d.mts → index.d.cts} +0 -0
- /package/dist/{index.mjs.map → node.js.map} +0 -0
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Filesystem Metadata Loader
|
|
5
|
-
*
|
|
6
|
-
* Loads metadata from the filesystem using glob patterns
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as fs from 'node:fs/promises';
|
|
10
|
-
import * as path from 'node:path';
|
|
11
|
-
import { glob } from 'glob';
|
|
12
|
-
import { createHash } from 'node:crypto';
|
|
13
|
-
import type {
|
|
14
|
-
MetadataLoadOptions,
|
|
15
|
-
MetadataLoadResult,
|
|
16
|
-
MetadataStats,
|
|
17
|
-
MetadataLoaderContract,
|
|
18
|
-
MetadataFormat,
|
|
19
|
-
MetadataSaveOptions,
|
|
20
|
-
MetadataSaveResult,
|
|
21
|
-
} from '@objectstack/spec/system';
|
|
22
|
-
import type { Logger } from '@objectstack/core';
|
|
23
|
-
import type { MetadataLoader } from './loader-interface.js';
|
|
24
|
-
import type { MetadataSerializer } from '../serializers/serializer-interface.js';
|
|
25
|
-
|
|
26
|
-
export class FilesystemLoader implements MetadataLoader {
|
|
27
|
-
readonly contract: MetadataLoaderContract = {
|
|
28
|
-
name: 'filesystem',
|
|
29
|
-
protocol: 'file:',
|
|
30
|
-
capabilities: {
|
|
31
|
-
read: true,
|
|
32
|
-
write: true,
|
|
33
|
-
watch: true,
|
|
34
|
-
list: true,
|
|
35
|
-
},
|
|
36
|
-
supportedFormats: ['json', 'yaml', 'typescript', 'javascript'],
|
|
37
|
-
supportsWatch: true,
|
|
38
|
-
supportsWrite: true,
|
|
39
|
-
supportsCache: true,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
private cache = new Map<string, { data: any; etag: string; timestamp: number }>();
|
|
43
|
-
|
|
44
|
-
constructor(
|
|
45
|
-
private rootDir: string,
|
|
46
|
-
private serializers: Map<MetadataFormat, MetadataSerializer>,
|
|
47
|
-
private logger?: Logger
|
|
48
|
-
) {}
|
|
49
|
-
|
|
50
|
-
async load(
|
|
51
|
-
type: string,
|
|
52
|
-
name: string,
|
|
53
|
-
options?: MetadataLoadOptions
|
|
54
|
-
): Promise<MetadataLoadResult> {
|
|
55
|
-
const startTime = Date.now();
|
|
56
|
-
const { validate: _validate = true, useCache = true, ifNoneMatch } = options || {};
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Find the file
|
|
60
|
-
const filePath = await this.findFile(type, name);
|
|
61
|
-
|
|
62
|
-
if (!filePath) {
|
|
63
|
-
return {
|
|
64
|
-
data: null,
|
|
65
|
-
fromCache: false,
|
|
66
|
-
notModified: false,
|
|
67
|
-
loadTime: Date.now() - startTime,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get stats
|
|
72
|
-
const stats = await this.stat(type, name);
|
|
73
|
-
|
|
74
|
-
if (!stats) {
|
|
75
|
-
return {
|
|
76
|
-
data: null,
|
|
77
|
-
fromCache: false,
|
|
78
|
-
notModified: false,
|
|
79
|
-
loadTime: Date.now() - startTime,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Check cache
|
|
84
|
-
if (useCache && ifNoneMatch && stats.etag === ifNoneMatch) {
|
|
85
|
-
return {
|
|
86
|
-
data: null,
|
|
87
|
-
fromCache: true,
|
|
88
|
-
notModified: true,
|
|
89
|
-
etag: stats.etag,
|
|
90
|
-
stats,
|
|
91
|
-
loadTime: Date.now() - startTime,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Check memory cache
|
|
96
|
-
const cacheKey = `${type}:${name}`;
|
|
97
|
-
if (useCache && this.cache.has(cacheKey)) {
|
|
98
|
-
const cached = this.cache.get(cacheKey)!;
|
|
99
|
-
if (cached.etag === stats.etag) {
|
|
100
|
-
return {
|
|
101
|
-
data: cached.data,
|
|
102
|
-
fromCache: true,
|
|
103
|
-
notModified: false,
|
|
104
|
-
etag: stats.etag,
|
|
105
|
-
stats,
|
|
106
|
-
loadTime: Date.now() - startTime,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Load and deserialize
|
|
112
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
113
|
-
const serializer = this.getSerializer(stats.format!);
|
|
114
|
-
|
|
115
|
-
if (!serializer) {
|
|
116
|
-
throw new Error(`No serializer found for format: ${stats.format}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const data = serializer.deserialize(content);
|
|
120
|
-
|
|
121
|
-
// Update cache
|
|
122
|
-
if (useCache) {
|
|
123
|
-
this.cache.set(cacheKey, {
|
|
124
|
-
data,
|
|
125
|
-
etag: stats.etag || '',
|
|
126
|
-
timestamp: Date.now(),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
data,
|
|
132
|
-
fromCache: false,
|
|
133
|
-
notModified: false,
|
|
134
|
-
etag: stats.etag,
|
|
135
|
-
stats,
|
|
136
|
-
loadTime: Date.now() - startTime,
|
|
137
|
-
};
|
|
138
|
-
} catch (error) {
|
|
139
|
-
this.logger?.error('Failed to load metadata', undefined, {
|
|
140
|
-
type,
|
|
141
|
-
name,
|
|
142
|
-
error: error instanceof Error ? error.message : String(error),
|
|
143
|
-
});
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async loadMany<T = any>(
|
|
149
|
-
type: string,
|
|
150
|
-
options?: MetadataLoadOptions
|
|
151
|
-
): Promise<T[]> {
|
|
152
|
-
const { patterns = ['**/*'], recursive: _recursive = true, limit } = options || {};
|
|
153
|
-
|
|
154
|
-
const typeDir = path.join(this.rootDir, type);
|
|
155
|
-
const items: T[] = [];
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
// Build glob patterns
|
|
159
|
-
const globPatterns = patterns.map(pattern =>
|
|
160
|
-
path.join(typeDir, pattern)
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
for (const pattern of globPatterns) {
|
|
164
|
-
const files = await glob(pattern, {
|
|
165
|
-
ignore: ['**/node_modules/**', '**/*.test.*', '**/*.spec.*'],
|
|
166
|
-
nodir: true,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
for (const file of files) {
|
|
170
|
-
if (limit && items.length >= limit) {
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const content = await fs.readFile(file, 'utf-8');
|
|
176
|
-
const format = this.detectFormat(file);
|
|
177
|
-
const serializer = this.getSerializer(format);
|
|
178
|
-
|
|
179
|
-
if (serializer) {
|
|
180
|
-
const data = serializer.deserialize<T>(content);
|
|
181
|
-
items.push(data);
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
this.logger?.warn('Failed to load file', {
|
|
185
|
-
file,
|
|
186
|
-
error: error instanceof Error ? error.message : String(error),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (limit && items.length >= limit) {
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return items;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
this.logger?.error('Failed to load many', undefined, {
|
|
199
|
-
type,
|
|
200
|
-
patterns,
|
|
201
|
-
error: error instanceof Error ? error.message : String(error),
|
|
202
|
-
});
|
|
203
|
-
throw error;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async exists(type: string, name: string): Promise<boolean> {
|
|
208
|
-
const filePath = await this.findFile(type, name);
|
|
209
|
-
return filePath !== null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async stat(type: string, name: string): Promise<MetadataStats | null> {
|
|
213
|
-
const filePath = await this.findFile(type, name);
|
|
214
|
-
|
|
215
|
-
if (!filePath) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const stats = await fs.stat(filePath);
|
|
221
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
222
|
-
const etag = this.generateETag(content);
|
|
223
|
-
const format = this.detectFormat(filePath);
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
size: stats.size,
|
|
227
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
228
|
-
etag,
|
|
229
|
-
format,
|
|
230
|
-
path: filePath,
|
|
231
|
-
};
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.logger?.error('Failed to stat file', undefined, {
|
|
234
|
-
type,
|
|
235
|
-
name,
|
|
236
|
-
filePath,
|
|
237
|
-
error: error instanceof Error ? error.message : String(error),
|
|
238
|
-
});
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async list(type: string): Promise<string[]> {
|
|
244
|
-
const typeDir = path.join(this.rootDir, type);
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const files = await glob('**/*', {
|
|
248
|
-
cwd: typeDir,
|
|
249
|
-
ignore: ['**/node_modules/**', '**/*.test.*', '**/*.spec.*'],
|
|
250
|
-
nodir: true,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return files.map(file => {
|
|
254
|
-
const ext = path.extname(file);
|
|
255
|
-
const basename = path.basename(file, ext);
|
|
256
|
-
return basename;
|
|
257
|
-
});
|
|
258
|
-
} catch (error) {
|
|
259
|
-
this.logger?.error('Failed to list', undefined, {
|
|
260
|
-
type,
|
|
261
|
-
error: error instanceof Error ? error.message : String(error),
|
|
262
|
-
});
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async save(
|
|
268
|
-
type: string,
|
|
269
|
-
name: string,
|
|
270
|
-
data: any,
|
|
271
|
-
options?: MetadataSaveOptions
|
|
272
|
-
): Promise<MetadataSaveResult> {
|
|
273
|
-
const startTime = Date.now();
|
|
274
|
-
const {
|
|
275
|
-
format = 'typescript',
|
|
276
|
-
prettify = true,
|
|
277
|
-
indent = 2,
|
|
278
|
-
sortKeys = false,
|
|
279
|
-
backup = false,
|
|
280
|
-
overwrite = true,
|
|
281
|
-
atomic = true,
|
|
282
|
-
path: customPath,
|
|
283
|
-
} = options || {};
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
// Get serializer
|
|
287
|
-
const serializer = this.getSerializer(format);
|
|
288
|
-
if (!serializer) {
|
|
289
|
-
throw new Error(`No serializer found for format: ${format}`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Determine file path
|
|
293
|
-
const typeDir = path.join(this.rootDir, type);
|
|
294
|
-
const fileName = `${name}${serializer.getExtension()}`;
|
|
295
|
-
const filePath = customPath || path.join(typeDir, fileName);
|
|
296
|
-
|
|
297
|
-
// Check if file exists
|
|
298
|
-
if (!overwrite) {
|
|
299
|
-
try {
|
|
300
|
-
await fs.access(filePath);
|
|
301
|
-
throw new Error(`File already exists: ${filePath}`);
|
|
302
|
-
} catch (error) {
|
|
303
|
-
// File doesn't exist, continue
|
|
304
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Create directory if it doesn't exist
|
|
311
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
312
|
-
|
|
313
|
-
// Create backup if requested
|
|
314
|
-
let backupPath: string | undefined;
|
|
315
|
-
if (backup) {
|
|
316
|
-
try {
|
|
317
|
-
await fs.access(filePath);
|
|
318
|
-
backupPath = `${filePath}.bak`;
|
|
319
|
-
await fs.copyFile(filePath, backupPath);
|
|
320
|
-
} catch {
|
|
321
|
-
// File doesn't exist, no backup needed
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Serialize data
|
|
326
|
-
const content = serializer.serialize(data, {
|
|
327
|
-
prettify,
|
|
328
|
-
indent,
|
|
329
|
-
sortKeys,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Write to disk (atomic or direct)
|
|
333
|
-
if (atomic) {
|
|
334
|
-
const tempPath = `${filePath}.tmp`;
|
|
335
|
-
await fs.writeFile(tempPath, content, 'utf-8');
|
|
336
|
-
await fs.rename(tempPath, filePath);
|
|
337
|
-
} else {
|
|
338
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Update cache logic if needed (e.g., invalidate or update)
|
|
342
|
-
// For now, we rely on the watcher to pick up changes
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
success: true,
|
|
346
|
-
path: filePath,
|
|
347
|
-
// format, // Not in schema
|
|
348
|
-
size: Buffer.byteLength(content, 'utf-8'),
|
|
349
|
-
backupPath,
|
|
350
|
-
saveTime: Date.now() - startTime,
|
|
351
|
-
};
|
|
352
|
-
} catch (error) {
|
|
353
|
-
this.logger?.error('Failed to save metadata', undefined, {
|
|
354
|
-
type,
|
|
355
|
-
name,
|
|
356
|
-
error: error instanceof Error ? error.message : String(error),
|
|
357
|
-
});
|
|
358
|
-
throw error;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Find file for a given type and name
|
|
364
|
-
*/
|
|
365
|
-
private async findFile(type: string, name: string): Promise<string | null> {
|
|
366
|
-
const typeDir = path.join(this.rootDir, type);
|
|
367
|
-
const extensions = ['.json', '.yaml', '.yml', '.ts', '.js'];
|
|
368
|
-
|
|
369
|
-
for (const ext of extensions) {
|
|
370
|
-
const filePath = path.join(typeDir, `${name}${ext}`);
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
await fs.access(filePath);
|
|
374
|
-
return filePath;
|
|
375
|
-
} catch {
|
|
376
|
-
// File doesn't exist, try next extension
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Detect format from file extension
|
|
385
|
-
*/
|
|
386
|
-
private detectFormat(filePath: string): MetadataFormat {
|
|
387
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
388
|
-
|
|
389
|
-
switch (ext) {
|
|
390
|
-
case '.json':
|
|
391
|
-
return 'json';
|
|
392
|
-
case '.yaml':
|
|
393
|
-
case '.yml':
|
|
394
|
-
return 'yaml';
|
|
395
|
-
case '.ts':
|
|
396
|
-
return 'typescript';
|
|
397
|
-
case '.js':
|
|
398
|
-
return 'javascript';
|
|
399
|
-
default:
|
|
400
|
-
return 'json'; // Default to JSON
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Get serializer for format
|
|
406
|
-
*/
|
|
407
|
-
private getSerializer(format: MetadataFormat): MetadataSerializer | undefined {
|
|
408
|
-
return this.serializers.get(format);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Generate ETag for content
|
|
413
|
-
* Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
|
|
414
|
-
* while keeping ETag headers compact (full 64-char hash is overkill for this use case)
|
|
415
|
-
*/
|
|
416
|
-
private generateETag(content: string): string {
|
|
417
|
-
const hash = createHash('sha256').update(content).digest('hex').substring(0, 32);
|
|
418
|
-
return `"${hash}"`;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Metadata Loader Interface
|
|
5
|
-
*
|
|
6
|
-
* Defines the contract for loading metadata from various sources
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
MetadataLoadOptions,
|
|
11
|
-
MetadataLoadResult,
|
|
12
|
-
MetadataStats,
|
|
13
|
-
MetadataLoaderContract,
|
|
14
|
-
MetadataSaveOptions,
|
|
15
|
-
MetadataSaveResult,
|
|
16
|
-
} from '@objectstack/spec/system';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Abstract interface for metadata loaders
|
|
20
|
-
* Implementations can load from filesystem, HTTP, S3, databases, etc.
|
|
21
|
-
*/
|
|
22
|
-
export interface MetadataLoader {
|
|
23
|
-
/**
|
|
24
|
-
* Loader contract information
|
|
25
|
-
*/
|
|
26
|
-
readonly contract: MetadataLoaderContract;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Load a single metadata item
|
|
30
|
-
* @param type The metadata type (e.g., 'object', 'view', 'app')
|
|
31
|
-
* @param name The item name/identifier
|
|
32
|
-
* @param options Load options
|
|
33
|
-
* @returns Load result with data or null if not found
|
|
34
|
-
*/
|
|
35
|
-
load(
|
|
36
|
-
type: string,
|
|
37
|
-
name: string,
|
|
38
|
-
options?: MetadataLoadOptions
|
|
39
|
-
): Promise<MetadataLoadResult>;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Load multiple items matching patterns
|
|
43
|
-
* @param type The metadata type
|
|
44
|
-
* @param options Load options with patterns
|
|
45
|
-
* @returns Array of loaded items
|
|
46
|
-
*/
|
|
47
|
-
loadMany<T = any>(
|
|
48
|
-
type: string,
|
|
49
|
-
options?: MetadataLoadOptions
|
|
50
|
-
): Promise<T[]>;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Check if item exists
|
|
54
|
-
* @param type The metadata type
|
|
55
|
-
* @param name The item name
|
|
56
|
-
* @returns True if exists
|
|
57
|
-
*/
|
|
58
|
-
exists(type: string, name: string): Promise<boolean>;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get item metadata (without loading full content)
|
|
62
|
-
* @param type The metadata type
|
|
63
|
-
* @param name The item name
|
|
64
|
-
* @returns Metadata statistics
|
|
65
|
-
*/
|
|
66
|
-
stat(type: string, name: string): Promise<MetadataStats | null>;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* List all items of a type
|
|
70
|
-
* @param type The metadata type
|
|
71
|
-
* @returns Array of item names
|
|
72
|
-
*/
|
|
73
|
-
list(type: string): Promise<string[]>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Save metadata item
|
|
77
|
-
* @param type The metadata type
|
|
78
|
-
* @param name The item name
|
|
79
|
-
* @param data The data to save
|
|
80
|
-
* @param options Save options
|
|
81
|
-
*/
|
|
82
|
-
save?(
|
|
83
|
-
type: string,
|
|
84
|
-
name: string,
|
|
85
|
-
data: any,
|
|
86
|
-
options?: MetadataSaveOptions
|
|
87
|
-
): Promise<MetadataSaveResult>;
|
|
88
|
-
}
|
|
89
|
-
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Memory Metadata Loader
|
|
5
|
-
*
|
|
6
|
-
* Stores metadata in memory only. Changes are lost when process restarts.
|
|
7
|
-
* Useful for testing, temporary overrides, or "dirty" edits.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
MetadataLoadOptions,
|
|
12
|
-
MetadataLoadResult,
|
|
13
|
-
MetadataStats,
|
|
14
|
-
MetadataLoaderContract,
|
|
15
|
-
MetadataSaveOptions,
|
|
16
|
-
MetadataSaveResult,
|
|
17
|
-
} from '@objectstack/spec/system';
|
|
18
|
-
import type { MetadataLoader } from './loader-interface.js';
|
|
19
|
-
|
|
20
|
-
export class MemoryLoader implements MetadataLoader {
|
|
21
|
-
readonly contract: MetadataLoaderContract = {
|
|
22
|
-
name: 'memory',
|
|
23
|
-
protocol: 'memory:',
|
|
24
|
-
capabilities: {
|
|
25
|
-
read: true,
|
|
26
|
-
write: true,
|
|
27
|
-
watch: false,
|
|
28
|
-
list: true,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Storage: Type -> Name -> Data
|
|
33
|
-
private storage = new Map<string, Map<string, any>>();
|
|
34
|
-
|
|
35
|
-
async load(
|
|
36
|
-
type: string,
|
|
37
|
-
name: string,
|
|
38
|
-
_options?: MetadataLoadOptions
|
|
39
|
-
): Promise<MetadataLoadResult> {
|
|
40
|
-
const typeStore = this.storage.get(type);
|
|
41
|
-
const data = typeStore?.get(name);
|
|
42
|
-
|
|
43
|
-
if (data) {
|
|
44
|
-
return {
|
|
45
|
-
data,
|
|
46
|
-
source: 'memory',
|
|
47
|
-
format: 'json',
|
|
48
|
-
loadTime: 0,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { data: null };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async loadMany<T = any>(
|
|
56
|
-
type: string,
|
|
57
|
-
_options?: MetadataLoadOptions
|
|
58
|
-
): Promise<T[]> {
|
|
59
|
-
const typeStore = this.storage.get(type);
|
|
60
|
-
if (!typeStore) return [];
|
|
61
|
-
return Array.from(typeStore.values()) as T[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async exists(type: string, name: string): Promise<boolean> {
|
|
65
|
-
return this.storage.get(type)?.has(name) ?? false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async stat(type: string, name: string): Promise<MetadataStats | null> {
|
|
69
|
-
if (await this.exists(type, name)) {
|
|
70
|
-
return {
|
|
71
|
-
size: 0, // In-memory
|
|
72
|
-
mtime: new Date().toISOString(),
|
|
73
|
-
format: 'json',
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async list(type: string): Promise<string[]> {
|
|
80
|
-
const typeStore = this.storage.get(type);
|
|
81
|
-
if (!typeStore) return [];
|
|
82
|
-
return Array.from(typeStore.keys());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async save(
|
|
86
|
-
type: string,
|
|
87
|
-
name: string,
|
|
88
|
-
data: any,
|
|
89
|
-
_options?: MetadataSaveOptions
|
|
90
|
-
): Promise<MetadataSaveResult> {
|
|
91
|
-
if (!this.storage.has(type)) {
|
|
92
|
-
this.storage.set(type, new Map());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.storage.get(type)!.set(name, data);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
success: true,
|
|
99
|
-
path: `memory://${type}/${name}`,
|
|
100
|
-
saveTime: 0,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}
|