@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.
Files changed (38) hide show
  1. package/dist/index.cjs +2197 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.js +42 -82
  4. package/dist/index.js.map +1 -1
  5. package/dist/node.cjs +2201 -0
  6. package/dist/node.cjs.map +1 -0
  7. package/dist/node.d.cts +65 -0
  8. package/dist/node.d.ts +65 -0
  9. package/dist/{index.mjs → node.js} +3 -1
  10. package/package.json +22 -17
  11. package/.turbo/turbo-build.log +0 -22
  12. package/CHANGELOG.md +0 -504
  13. package/ROADMAP.md +0 -224
  14. package/src/index.ts +0 -68
  15. package/src/loaders/database-loader.test.ts +0 -559
  16. package/src/loaders/database-loader.ts +0 -352
  17. package/src/loaders/filesystem-loader.ts +0 -420
  18. package/src/loaders/loader-interface.ts +0 -89
  19. package/src/loaders/memory-loader.ts +0 -103
  20. package/src/loaders/remote-loader.ts +0 -140
  21. package/src/metadata-manager.ts +0 -1168
  22. package/src/metadata-service.test.ts +0 -965
  23. package/src/metadata.test.ts +0 -431
  24. package/src/migration/executor.ts +0 -54
  25. package/src/migration/index.ts +0 -3
  26. package/src/node-metadata-manager.ts +0 -126
  27. package/src/node.ts +0 -11
  28. package/src/objects/sys-metadata.object.ts +0 -188
  29. package/src/plugin.ts +0 -102
  30. package/src/serializers/json-serializer.ts +0 -73
  31. package/src/serializers/serializer-interface.ts +0 -65
  32. package/src/serializers/serializers.test.ts +0 -74
  33. package/src/serializers/typescript-serializer.ts +0 -127
  34. package/src/serializers/yaml-serializer.ts +0 -49
  35. package/tsconfig.json +0 -9
  36. package/vitest.config.ts +0 -23
  37. /package/dist/{index.d.mts → index.d.cts} +0 -0
  38. /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
- }