@objectstack/metadata 1.0.4 → 1.0.6

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 (56) hide show
  1. package/.turbo/turbo-build.log +22 -0
  2. package/CHANGELOG.md +23 -0
  3. package/dist/index.d.mts +306 -0
  4. package/dist/index.d.ts +305 -16
  5. package/dist/index.js +1078 -15
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +1040 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +7 -7
  10. package/tsconfig.json +1 -3
  11. package/dist/index.d.ts.map +0 -1
  12. package/dist/loaders/filesystem-loader.d.ts +0 -42
  13. package/dist/loaders/filesystem-loader.d.ts.map +0 -1
  14. package/dist/loaders/filesystem-loader.js +0 -342
  15. package/dist/loaders/loader-interface.d.ts +0 -60
  16. package/dist/loaders/loader-interface.d.ts.map +0 -1
  17. package/dist/loaders/loader-interface.js +0 -6
  18. package/dist/loaders/memory-loader.d.ts +0 -19
  19. package/dist/loaders/memory-loader.d.ts.map +0 -1
  20. package/dist/loaders/memory-loader.js +0 -71
  21. package/dist/loaders/remote-loader.d.ts +0 -22
  22. package/dist/loaders/remote-loader.d.ts.map +0 -1
  23. package/dist/loaders/remote-loader.js +0 -103
  24. package/dist/metadata-manager.d.ts +0 -71
  25. package/dist/metadata-manager.d.ts.map +0 -1
  26. package/dist/metadata-manager.js +0 -211
  27. package/dist/migration/executor.d.ts +0 -9
  28. package/dist/migration/executor.d.ts.map +0 -1
  29. package/dist/migration/executor.js +0 -49
  30. package/dist/migration/index.d.ts +0 -2
  31. package/dist/migration/index.d.ts.map +0 -1
  32. package/dist/migration/index.js +0 -1
  33. package/dist/node-metadata-manager.d.ts +0 -26
  34. package/dist/node-metadata-manager.d.ts.map +0 -1
  35. package/dist/node-metadata-manager.js +0 -98
  36. package/dist/node.d.ts +0 -8
  37. package/dist/node.d.ts.map +0 -1
  38. package/dist/node.js +0 -7
  39. package/dist/plugin.d.ts +0 -15
  40. package/dist/plugin.d.ts.map +0 -1
  41. package/dist/plugin.js +0 -71
  42. package/dist/serializers/json-serializer.d.ts +0 -20
  43. package/dist/serializers/json-serializer.d.ts.map +0 -1
  44. package/dist/serializers/json-serializer.js +0 -53
  45. package/dist/serializers/serializer-interface.d.ts +0 -57
  46. package/dist/serializers/serializer-interface.d.ts.map +0 -1
  47. package/dist/serializers/serializer-interface.js +0 -6
  48. package/dist/serializers/serializers.test.d.ts +0 -2
  49. package/dist/serializers/serializers.test.d.ts.map +0 -1
  50. package/dist/serializers/serializers.test.js +0 -62
  51. package/dist/serializers/typescript-serializer.d.ts +0 -18
  52. package/dist/serializers/typescript-serializer.d.ts.map +0 -1
  53. package/dist/serializers/typescript-serializer.js +0 -103
  54. package/dist/serializers/yaml-serializer.d.ts +0 -16
  55. package/dist/serializers/yaml-serializer.d.ts.map +0 -1
  56. package/dist/serializers/yaml-serializer.js +0 -35
package/dist/index.mjs ADDED
@@ -0,0 +1,1040 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/metadata-manager.ts
8
+ import { createLogger } from "@objectstack/core";
9
+
10
+ // src/serializers/json-serializer.ts
11
+ var JSONSerializer = class {
12
+ serialize(item, options) {
13
+ const { prettify = true, indent = 2, sortKeys = false } = options || {};
14
+ if (sortKeys) {
15
+ const sorted = this.sortObjectKeys(item);
16
+ return prettify ? JSON.stringify(sorted, null, indent) : JSON.stringify(sorted);
17
+ }
18
+ return prettify ? JSON.stringify(item, null, indent) : JSON.stringify(item);
19
+ }
20
+ deserialize(content, schema) {
21
+ const parsed = JSON.parse(content);
22
+ if (schema) {
23
+ return schema.parse(parsed);
24
+ }
25
+ return parsed;
26
+ }
27
+ getExtension() {
28
+ return ".json";
29
+ }
30
+ canHandle(format) {
31
+ return format === "json";
32
+ }
33
+ getFormat() {
34
+ return "json";
35
+ }
36
+ /**
37
+ * Recursively sort object keys
38
+ */
39
+ sortObjectKeys(obj) {
40
+ if (obj === null || typeof obj !== "object") {
41
+ return obj;
42
+ }
43
+ if (Array.isArray(obj)) {
44
+ return obj.map((item) => this.sortObjectKeys(item));
45
+ }
46
+ const sorted = {};
47
+ const keys = Object.keys(obj).sort();
48
+ for (const key of keys) {
49
+ sorted[key] = this.sortObjectKeys(obj[key]);
50
+ }
51
+ return sorted;
52
+ }
53
+ };
54
+
55
+ // src/serializers/yaml-serializer.ts
56
+ import * as yaml from "js-yaml";
57
+ var YAMLSerializer = class {
58
+ serialize(item, options) {
59
+ const { indent = 2, sortKeys = false } = options || {};
60
+ return yaml.dump(item, {
61
+ indent,
62
+ sortKeys,
63
+ lineWidth: -1,
64
+ // Disable line wrapping
65
+ noRefs: true
66
+ // Disable YAML references
67
+ });
68
+ }
69
+ deserialize(content, schema) {
70
+ const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
71
+ if (schema) {
72
+ return schema.parse(parsed);
73
+ }
74
+ return parsed;
75
+ }
76
+ getExtension() {
77
+ return ".yaml";
78
+ }
79
+ canHandle(format) {
80
+ return format === "yaml";
81
+ }
82
+ getFormat() {
83
+ return "yaml";
84
+ }
85
+ };
86
+
87
+ // src/serializers/typescript-serializer.ts
88
+ var TypeScriptSerializer = class {
89
+ constructor(format = "typescript") {
90
+ this.format = format;
91
+ }
92
+ serialize(item, options) {
93
+ const { prettify = true, indent = 2 } = options || {};
94
+ const jsonStr = JSON.stringify(item, null, prettify ? indent : 0);
95
+ if (this.format === "typescript") {
96
+ return `import type { ServiceObject } from '@objectstack/spec/data';
97
+
98
+ export const metadata: ServiceObject = ${jsonStr};
99
+
100
+ export default metadata;
101
+ `;
102
+ } else {
103
+ return `export const metadata = ${jsonStr};
104
+
105
+ export default metadata;
106
+ `;
107
+ }
108
+ }
109
+ deserialize(content, schema) {
110
+ let objectStart = content.indexOf("export const");
111
+ if (objectStart === -1) {
112
+ objectStart = content.indexOf("export default");
113
+ }
114
+ if (objectStart === -1) {
115
+ throw new Error(
116
+ 'Could not parse TypeScript/JavaScript module. Expected export pattern: "export const metadata = {...};" or "export default {...};"'
117
+ );
118
+ }
119
+ const braceStart = content.indexOf("{", objectStart);
120
+ if (braceStart === -1) {
121
+ throw new Error("Could not find object literal in export statement");
122
+ }
123
+ let braceCount = 0;
124
+ let braceEnd = -1;
125
+ let inString = false;
126
+ let stringChar = "";
127
+ for (let i = braceStart; i < content.length; i++) {
128
+ const char = content[i];
129
+ const prevChar = i > 0 ? content[i - 1] : "";
130
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
131
+ if (!inString) {
132
+ inString = true;
133
+ stringChar = char;
134
+ } else if (char === stringChar) {
135
+ inString = false;
136
+ stringChar = "";
137
+ }
138
+ }
139
+ if (!inString) {
140
+ if (char === "{") braceCount++;
141
+ if (char === "}") {
142
+ braceCount--;
143
+ if (braceCount === 0) {
144
+ braceEnd = i;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ if (braceEnd === -1) {
151
+ throw new Error("Could not find matching closing brace for object literal");
152
+ }
153
+ const objectLiteral = content.substring(braceStart, braceEnd + 1);
154
+ try {
155
+ const parsed = JSON.parse(objectLiteral);
156
+ if (schema) {
157
+ return schema.parse(parsed);
158
+ }
159
+ return parsed;
160
+ } catch (error) {
161
+ throw new Error(
162
+ `Failed to parse object literal as JSON: ${error instanceof Error ? error.message : String(error)}. Make sure the TypeScript/JavaScript object uses JSON-compatible syntax (no functions, comments, or trailing commas).`
163
+ );
164
+ }
165
+ }
166
+ getExtension() {
167
+ return this.format === "typescript" ? ".ts" : ".js";
168
+ }
169
+ canHandle(format) {
170
+ return format === "typescript" || format === "javascript";
171
+ }
172
+ getFormat() {
173
+ return this.format;
174
+ }
175
+ };
176
+
177
+ // src/metadata-manager.ts
178
+ var MetadataManager = class {
179
+ constructor(config) {
180
+ this.loaders = /* @__PURE__ */ new Map();
181
+ this.watchCallbacks = /* @__PURE__ */ new Map();
182
+ this.config = config;
183
+ this.logger = createLogger({ level: "info", format: "pretty" });
184
+ this.serializers = /* @__PURE__ */ new Map();
185
+ const formats = config.formats || ["typescript", "json", "yaml"];
186
+ if (formats.includes("json")) {
187
+ this.serializers.set("json", new JSONSerializer());
188
+ }
189
+ if (formats.includes("yaml")) {
190
+ this.serializers.set("yaml", new YAMLSerializer());
191
+ }
192
+ if (formats.includes("typescript")) {
193
+ this.serializers.set("typescript", new TypeScriptSerializer("typescript"));
194
+ }
195
+ if (formats.includes("javascript")) {
196
+ this.serializers.set("javascript", new TypeScriptSerializer("javascript"));
197
+ }
198
+ if (config.loaders && config.loaders.length > 0) {
199
+ config.loaders.forEach((loader) => this.registerLoader(loader));
200
+ }
201
+ }
202
+ /**
203
+ * Register a new metadata loader (data source)
204
+ */
205
+ registerLoader(loader) {
206
+ this.loaders.set(loader.contract.name, loader);
207
+ this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
208
+ }
209
+ /**
210
+ * Load a single metadata item
211
+ * Iterates through registered loaders until found
212
+ */
213
+ async load(type, name, options) {
214
+ for (const loader of this.loaders.values()) {
215
+ try {
216
+ const result = await loader.load(type, name, options);
217
+ if (result.data) {
218
+ return result.data;
219
+ }
220
+ } catch (e) {
221
+ this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+ /**
227
+ * Load multiple metadata items
228
+ * Aggregates results from all loaders
229
+ */
230
+ async loadMany(type, options) {
231
+ const results = [];
232
+ for (const loader of this.loaders.values()) {
233
+ try {
234
+ const items = await loader.loadMany(type, options);
235
+ for (const item of items) {
236
+ results.push(item);
237
+ }
238
+ } catch (e) {
239
+ this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
240
+ }
241
+ }
242
+ return results;
243
+ }
244
+ /**
245
+ * Save metadata to disk
246
+ */
247
+ /**
248
+ * Save metadata item
249
+ */
250
+ async save(type, name, data, options) {
251
+ const targetLoader = options?.loader;
252
+ let loader;
253
+ if (targetLoader) {
254
+ loader = this.loaders.get(targetLoader);
255
+ if (!loader) {
256
+ throw new Error(`Loader not found: ${targetLoader}`);
257
+ }
258
+ } else {
259
+ for (const l of this.loaders.values()) {
260
+ if (!l.save) continue;
261
+ try {
262
+ if (await l.exists(type, name)) {
263
+ loader = l;
264
+ this.logger.info(`Updating existing metadata in loader: ${l.contract.name}`);
265
+ break;
266
+ }
267
+ } catch (e) {
268
+ }
269
+ }
270
+ if (!loader) {
271
+ const fsLoader = this.loaders.get("filesystem");
272
+ if (fsLoader && fsLoader.save) {
273
+ loader = fsLoader;
274
+ }
275
+ }
276
+ if (!loader) {
277
+ for (const l of this.loaders.values()) {
278
+ if (l.save) {
279
+ loader = l;
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ if (!loader) {
286
+ throw new Error(`No loader available for saving type: ${type}`);
287
+ }
288
+ if (!loader.save) {
289
+ throw new Error(`Loader '${loader.contract?.name}' does not support saving`);
290
+ }
291
+ return loader.save(type, name, data, options);
292
+ }
293
+ /**
294
+ * Check if metadata item exists
295
+ */
296
+ async exists(type, name) {
297
+ for (const loader of this.loaders.values()) {
298
+ if (await loader.exists(type, name)) {
299
+ return true;
300
+ }
301
+ }
302
+ return false;
303
+ }
304
+ /**
305
+ * List all items of a type
306
+ */
307
+ async list(type) {
308
+ const items = /* @__PURE__ */ new Set();
309
+ for (const loader of this.loaders.values()) {
310
+ const result = await loader.list(type);
311
+ result.forEach((item) => items.add(item));
312
+ }
313
+ return Array.from(items);
314
+ }
315
+ /**
316
+ * Watch for metadata changes
317
+ */
318
+ watch(type, callback) {
319
+ if (!this.watchCallbacks.has(type)) {
320
+ this.watchCallbacks.set(type, /* @__PURE__ */ new Set());
321
+ }
322
+ this.watchCallbacks.get(type).add(callback);
323
+ }
324
+ /**
325
+ * Unwatch metadata changes
326
+ */
327
+ unwatch(type, callback) {
328
+ const callbacks = this.watchCallbacks.get(type);
329
+ if (callbacks) {
330
+ callbacks.delete(callback);
331
+ if (callbacks.size === 0) {
332
+ this.watchCallbacks.delete(type);
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Stop all watching
338
+ */
339
+ async stopWatching() {
340
+ }
341
+ notifyWatchers(type, event) {
342
+ const callbacks = this.watchCallbacks.get(type);
343
+ if (!callbacks) return;
344
+ for (const callback of callbacks) {
345
+ try {
346
+ void callback(event);
347
+ } catch (error) {
348
+ this.logger.error("Watch callback error", void 0, {
349
+ type,
350
+ error: error instanceof Error ? error.message : String(error)
351
+ });
352
+ }
353
+ }
354
+ }
355
+ };
356
+
357
+ // src/node-metadata-manager.ts
358
+ import * as path2 from "path";
359
+ import { watch as chokidarWatch } from "chokidar";
360
+
361
+ // src/loaders/filesystem-loader.ts
362
+ import * as fs from "fs/promises";
363
+ import * as path from "path";
364
+ import { glob } from "glob";
365
+ import { createHash } from "crypto";
366
+ var FilesystemLoader = class {
367
+ constructor(rootDir, serializers, logger) {
368
+ this.rootDir = rootDir;
369
+ this.serializers = serializers;
370
+ this.logger = logger;
371
+ this.contract = {
372
+ name: "filesystem",
373
+ protocol: "file",
374
+ capabilities: {
375
+ read: true,
376
+ write: true,
377
+ watch: true,
378
+ list: true
379
+ },
380
+ supportedFormats: ["json", "yaml", "typescript", "javascript"],
381
+ supportsWatch: true,
382
+ supportsWrite: true,
383
+ supportsCache: true
384
+ };
385
+ this.cache = /* @__PURE__ */ new Map();
386
+ }
387
+ async load(type, name, options) {
388
+ const startTime = Date.now();
389
+ const { validate: _validate = true, useCache = true, ifNoneMatch } = options || {};
390
+ try {
391
+ const filePath = await this.findFile(type, name);
392
+ if (!filePath) {
393
+ return {
394
+ data: null,
395
+ fromCache: false,
396
+ notModified: false,
397
+ loadTime: Date.now() - startTime
398
+ };
399
+ }
400
+ const stats = await this.stat(type, name);
401
+ if (!stats) {
402
+ return {
403
+ data: null,
404
+ fromCache: false,
405
+ notModified: false,
406
+ loadTime: Date.now() - startTime
407
+ };
408
+ }
409
+ if (useCache && ifNoneMatch && stats.etag === ifNoneMatch) {
410
+ return {
411
+ data: null,
412
+ fromCache: true,
413
+ notModified: true,
414
+ etag: stats.etag,
415
+ stats,
416
+ loadTime: Date.now() - startTime
417
+ };
418
+ }
419
+ const cacheKey = `${type}:${name}`;
420
+ if (useCache && this.cache.has(cacheKey)) {
421
+ const cached = this.cache.get(cacheKey);
422
+ if (cached.etag === stats.etag) {
423
+ return {
424
+ data: cached.data,
425
+ fromCache: true,
426
+ notModified: false,
427
+ etag: stats.etag,
428
+ stats,
429
+ loadTime: Date.now() - startTime
430
+ };
431
+ }
432
+ }
433
+ const content = await fs.readFile(filePath, "utf-8");
434
+ const serializer = this.getSerializer(stats.format);
435
+ if (!serializer) {
436
+ throw new Error(`No serializer found for format: ${stats.format}`);
437
+ }
438
+ const data = serializer.deserialize(content);
439
+ if (useCache) {
440
+ this.cache.set(cacheKey, {
441
+ data,
442
+ etag: stats.etag || "",
443
+ timestamp: Date.now()
444
+ });
445
+ }
446
+ return {
447
+ data,
448
+ fromCache: false,
449
+ notModified: false,
450
+ etag: stats.etag,
451
+ stats,
452
+ loadTime: Date.now() - startTime
453
+ };
454
+ } catch (error) {
455
+ this.logger?.error("Failed to load metadata", void 0, {
456
+ type,
457
+ name,
458
+ error: error instanceof Error ? error.message : String(error)
459
+ });
460
+ throw error;
461
+ }
462
+ }
463
+ async loadMany(type, options) {
464
+ const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
465
+ const typeDir = path.join(this.rootDir, type);
466
+ const items = [];
467
+ try {
468
+ const globPatterns = patterns.map(
469
+ (pattern) => path.join(typeDir, pattern)
470
+ );
471
+ for (const pattern of globPatterns) {
472
+ const files = await glob(pattern, {
473
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
474
+ nodir: true
475
+ });
476
+ for (const file of files) {
477
+ if (limit && items.length >= limit) {
478
+ break;
479
+ }
480
+ try {
481
+ const content = await fs.readFile(file, "utf-8");
482
+ const format = this.detectFormat(file);
483
+ const serializer = this.getSerializer(format);
484
+ if (serializer) {
485
+ const data = serializer.deserialize(content);
486
+ items.push(data);
487
+ }
488
+ } catch (error) {
489
+ this.logger?.warn("Failed to load file", {
490
+ file,
491
+ error: error instanceof Error ? error.message : String(error)
492
+ });
493
+ }
494
+ }
495
+ if (limit && items.length >= limit) {
496
+ break;
497
+ }
498
+ }
499
+ return items;
500
+ } catch (error) {
501
+ this.logger?.error("Failed to load many", void 0, {
502
+ type,
503
+ patterns,
504
+ error: error instanceof Error ? error.message : String(error)
505
+ });
506
+ throw error;
507
+ }
508
+ }
509
+ async exists(type, name) {
510
+ const filePath = await this.findFile(type, name);
511
+ return filePath !== null;
512
+ }
513
+ async stat(type, name) {
514
+ const filePath = await this.findFile(type, name);
515
+ if (!filePath) {
516
+ return null;
517
+ }
518
+ try {
519
+ const stats = await fs.stat(filePath);
520
+ const content = await fs.readFile(filePath, "utf-8");
521
+ const etag = this.generateETag(content);
522
+ const format = this.detectFormat(filePath);
523
+ return {
524
+ size: stats.size,
525
+ modifiedAt: stats.mtime,
526
+ etag,
527
+ format,
528
+ path: filePath
529
+ };
530
+ } catch (error) {
531
+ this.logger?.error("Failed to stat file", void 0, {
532
+ type,
533
+ name,
534
+ filePath,
535
+ error: error instanceof Error ? error.message : String(error)
536
+ });
537
+ return null;
538
+ }
539
+ }
540
+ async list(type) {
541
+ const typeDir = path.join(this.rootDir, type);
542
+ try {
543
+ const files = await glob("**/*", {
544
+ cwd: typeDir,
545
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
546
+ nodir: true
547
+ });
548
+ return files.map((file) => {
549
+ const ext = path.extname(file);
550
+ const basename3 = path.basename(file, ext);
551
+ return basename3;
552
+ });
553
+ } catch (error) {
554
+ this.logger?.error("Failed to list", void 0, {
555
+ type,
556
+ error: error instanceof Error ? error.message : String(error)
557
+ });
558
+ return [];
559
+ }
560
+ }
561
+ async save(type, name, data, options) {
562
+ const startTime = Date.now();
563
+ const {
564
+ format = "typescript",
565
+ prettify = true,
566
+ indent = 2,
567
+ sortKeys = false,
568
+ backup = false,
569
+ overwrite = true,
570
+ atomic = true,
571
+ path: customPath
572
+ } = options || {};
573
+ try {
574
+ const serializer = this.getSerializer(format);
575
+ if (!serializer) {
576
+ throw new Error(`No serializer found for format: ${format}`);
577
+ }
578
+ const typeDir = path.join(this.rootDir, type);
579
+ const fileName = `${name}${serializer.getExtension()}`;
580
+ const filePath = customPath || path.join(typeDir, fileName);
581
+ if (!overwrite) {
582
+ try {
583
+ await fs.access(filePath);
584
+ throw new Error(`File already exists: ${filePath}`);
585
+ } catch (error) {
586
+ if (error.code !== "ENOENT") {
587
+ throw error;
588
+ }
589
+ }
590
+ }
591
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
592
+ let backupPath;
593
+ if (backup) {
594
+ try {
595
+ await fs.access(filePath);
596
+ backupPath = `${filePath}.bak`;
597
+ await fs.copyFile(filePath, backupPath);
598
+ } catch {
599
+ }
600
+ }
601
+ const content = serializer.serialize(data, {
602
+ prettify,
603
+ indent,
604
+ sortKeys
605
+ });
606
+ if (atomic) {
607
+ const tempPath = `${filePath}.tmp`;
608
+ await fs.writeFile(tempPath, content, "utf-8");
609
+ await fs.rename(tempPath, filePath);
610
+ } else {
611
+ await fs.writeFile(filePath, content, "utf-8");
612
+ }
613
+ return {
614
+ success: true,
615
+ path: filePath,
616
+ // format, // Not in schema
617
+ size: Buffer.byteLength(content, "utf-8"),
618
+ backupPath,
619
+ saveTime: Date.now() - startTime
620
+ };
621
+ } catch (error) {
622
+ this.logger?.error("Failed to save metadata", void 0, {
623
+ type,
624
+ name,
625
+ error: error instanceof Error ? error.message : String(error)
626
+ });
627
+ throw error;
628
+ }
629
+ }
630
+ /**
631
+ * Find file for a given type and name
632
+ */
633
+ async findFile(type, name) {
634
+ const typeDir = path.join(this.rootDir, type);
635
+ const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
636
+ for (const ext of extensions) {
637
+ const filePath = path.join(typeDir, `${name}${ext}`);
638
+ try {
639
+ await fs.access(filePath);
640
+ return filePath;
641
+ } catch {
642
+ }
643
+ }
644
+ return null;
645
+ }
646
+ /**
647
+ * Detect format from file extension
648
+ */
649
+ detectFormat(filePath) {
650
+ const ext = path.extname(filePath).toLowerCase();
651
+ switch (ext) {
652
+ case ".json":
653
+ return "json";
654
+ case ".yaml":
655
+ case ".yml":
656
+ return "yaml";
657
+ case ".ts":
658
+ return "typescript";
659
+ case ".js":
660
+ return "javascript";
661
+ default:
662
+ return "json";
663
+ }
664
+ }
665
+ /**
666
+ * Get serializer for format
667
+ */
668
+ getSerializer(format) {
669
+ return this.serializers.get(format);
670
+ }
671
+ /**
672
+ * Generate ETag for content
673
+ * Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
674
+ * while keeping ETag headers compact (full 64-char hash is overkill for this use case)
675
+ */
676
+ generateETag(content) {
677
+ const hash = createHash("sha256").update(content).digest("hex").substring(0, 32);
678
+ return `"${hash}"`;
679
+ }
680
+ };
681
+
682
+ // src/node-metadata-manager.ts
683
+ var NodeMetadataManager = class extends MetadataManager {
684
+ constructor(config) {
685
+ super(config);
686
+ if (!config.loaders || config.loaders.length === 0) {
687
+ const rootDir = config.rootDir || process.cwd();
688
+ this.registerLoader(new FilesystemLoader(rootDir, this.serializers, this.logger));
689
+ }
690
+ if (config.watch) {
691
+ this.startWatching();
692
+ }
693
+ }
694
+ /**
695
+ * Stop all watching
696
+ */
697
+ async stopWatching() {
698
+ if (this.watcher) {
699
+ await this.watcher.close();
700
+ this.watcher = void 0;
701
+ }
702
+ }
703
+ /**
704
+ * Start watching for file changes
705
+ */
706
+ startWatching() {
707
+ const rootDir = this.config.rootDir || process.cwd();
708
+ const { ignored = ["**/node_modules/**", "**/*.test.*"], persistent = true } = this.config.watchOptions || {};
709
+ this.watcher = chokidarWatch(rootDir, {
710
+ ignored,
711
+ persistent,
712
+ ignoreInitial: true
713
+ });
714
+ this.watcher.on("add", async (filePath) => {
715
+ await this.handleFileEvent("added", filePath);
716
+ });
717
+ this.watcher.on("change", async (filePath) => {
718
+ await this.handleFileEvent("changed", filePath);
719
+ });
720
+ this.watcher.on("unlink", async (filePath) => {
721
+ await this.handleFileEvent("deleted", filePath);
722
+ });
723
+ this.logger.info("File watcher started", { rootDir });
724
+ }
725
+ /**
726
+ * Handle file change events
727
+ */
728
+ async handleFileEvent(eventType, filePath) {
729
+ const rootDir = this.config.rootDir || process.cwd();
730
+ const relativePath = path2.relative(rootDir, filePath);
731
+ const parts = relativePath.split(path2.sep);
732
+ if (parts.length < 2) {
733
+ return;
734
+ }
735
+ const type = parts[0];
736
+ const fileName = parts[parts.length - 1];
737
+ const name = path2.basename(fileName, path2.extname(fileName));
738
+ let data = void 0;
739
+ if (eventType !== "deleted") {
740
+ try {
741
+ data = await this.load(type, name, { useCache: false });
742
+ } catch (error) {
743
+ this.logger.error("Failed to load changed file", void 0, {
744
+ filePath,
745
+ error: error instanceof Error ? error.message : String(error)
746
+ });
747
+ return;
748
+ }
749
+ }
750
+ const event = {
751
+ type: eventType,
752
+ metadataType: type,
753
+ name,
754
+ path: filePath,
755
+ data,
756
+ timestamp: /* @__PURE__ */ new Date()
757
+ };
758
+ this.notifyWatchers(type, event);
759
+ }
760
+ };
761
+
762
+ // src/plugin.ts
763
+ import { ObjectStackDefinitionSchema } from "@objectstack/spec";
764
+ var MetadataPlugin = class {
765
+ constructor(options = {}) {
766
+ this.name = "com.objectstack.metadata";
767
+ this.version = "1.0.0";
768
+ this.init = async (ctx) => {
769
+ ctx.logger.info("Initializing Metadata Manager", { root: this.options.rootDir || process.cwd() });
770
+ ctx.registerService("metadata", this.manager);
771
+ };
772
+ this.start = async (ctx) => {
773
+ ctx.logger.info("Loading metadata...");
774
+ const metadataTypes = Object.keys(ObjectStackDefinitionSchema.shape).filter((key) => key !== "manifest");
775
+ for (const type of metadataTypes) {
776
+ try {
777
+ const items = await this.manager.loadMany(type, {
778
+ recursive: true
779
+ });
780
+ if (items.length > 0) {
781
+ ctx.logger.info(`Loaded ${items.length} ${type}`);
782
+ const ql = ctx.getService("objectql");
783
+ if (ql && ql.registry) {
784
+ items.forEach((item) => {
785
+ const keyField = item.id ? "id" : "name";
786
+ let registryType = type;
787
+ if (type === "objects") registryType = "object";
788
+ if (type === "apps") registryType = "app";
789
+ if (type === "plugins") registryType = "plugin";
790
+ if (type === "functions") registryType = "function";
791
+ ql.registry.registerItem(registryType, item, keyField);
792
+ });
793
+ }
794
+ }
795
+ } catch (e) {
796
+ }
797
+ }
798
+ };
799
+ this.options = {
800
+ watch: true,
801
+ ...options
802
+ };
803
+ const rootDir = this.options.rootDir || process.cwd();
804
+ this.manager = new NodeMetadataManager({
805
+ rootDir,
806
+ watch: this.options.watch ?? true,
807
+ formats: ["yaml", "json", "typescript", "javascript"]
808
+ });
809
+ }
810
+ };
811
+
812
+ // src/loaders/memory-loader.ts
813
+ var MemoryLoader = class {
814
+ constructor() {
815
+ this.contract = {
816
+ name: "memory",
817
+ protocol: "memory",
818
+ capabilities: {
819
+ read: true,
820
+ write: true,
821
+ watch: false,
822
+ list: true
823
+ }
824
+ };
825
+ // Storage: Type -> Name -> Data
826
+ this.storage = /* @__PURE__ */ new Map();
827
+ }
828
+ async load(type, name, _options) {
829
+ const typeStore = this.storage.get(type);
830
+ const data = typeStore?.get(name);
831
+ if (data) {
832
+ return {
833
+ data,
834
+ source: "memory",
835
+ format: "json",
836
+ loadTime: 0
837
+ };
838
+ }
839
+ return { data: null };
840
+ }
841
+ async loadMany(type, _options) {
842
+ const typeStore = this.storage.get(type);
843
+ if (!typeStore) return [];
844
+ return Array.from(typeStore.values());
845
+ }
846
+ async exists(type, name) {
847
+ return this.storage.get(type)?.has(name) ?? false;
848
+ }
849
+ async stat(type, name) {
850
+ if (await this.exists(type, name)) {
851
+ return {
852
+ size: 0,
853
+ // In-memory
854
+ mtime: /* @__PURE__ */ new Date(),
855
+ format: "json"
856
+ };
857
+ }
858
+ return null;
859
+ }
860
+ async list(type) {
861
+ const typeStore = this.storage.get(type);
862
+ if (!typeStore) return [];
863
+ return Array.from(typeStore.keys());
864
+ }
865
+ async save(type, name, data, _options) {
866
+ if (!this.storage.has(type)) {
867
+ this.storage.set(type, /* @__PURE__ */ new Map());
868
+ }
869
+ this.storage.get(type).set(name, data);
870
+ return {
871
+ success: true,
872
+ path: `memory://${type}/${name}`,
873
+ saveTime: 0
874
+ };
875
+ }
876
+ };
877
+
878
+ // src/loaders/remote-loader.ts
879
+ var RemoteLoader = class {
880
+ constructor(baseUrl, authToken) {
881
+ this.baseUrl = baseUrl;
882
+ this.authToken = authToken;
883
+ this.contract = {
884
+ name: "remote",
885
+ protocol: "http",
886
+ capabilities: {
887
+ read: true,
888
+ write: true,
889
+ watch: false,
890
+ // Could implement SSE/WebSocket in future
891
+ list: true
892
+ }
893
+ };
894
+ }
895
+ get headers() {
896
+ return {
897
+ "Content-Type": "application/json",
898
+ ...this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {}
899
+ };
900
+ }
901
+ async load(type, name, _options) {
902
+ try {
903
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
904
+ method: "GET",
905
+ headers: this.headers
906
+ });
907
+ if (response.status === 404) {
908
+ return { data: null };
909
+ }
910
+ if (!response.ok) {
911
+ throw new Error(`Remote load failed: ${response.statusText}`);
912
+ }
913
+ const data = await response.json();
914
+ return {
915
+ data,
916
+ source: this.baseUrl,
917
+ format: "json",
918
+ loadTime: 0
919
+ };
920
+ } catch (error) {
921
+ console.error(`RemoteLoader error loading ${type}/${name}`, error);
922
+ throw error;
923
+ }
924
+ }
925
+ async loadMany(type, _options) {
926
+ const response = await fetch(`${this.baseUrl}/${type}`, {
927
+ method: "GET",
928
+ headers: this.headers
929
+ });
930
+ if (!response.ok) {
931
+ return [];
932
+ }
933
+ return await response.json();
934
+ }
935
+ async exists(type, name) {
936
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
937
+ method: "HEAD",
938
+ headers: this.headers
939
+ });
940
+ return response.ok;
941
+ }
942
+ async stat(type, name) {
943
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
944
+ method: "HEAD",
945
+ headers: this.headers
946
+ });
947
+ if (!response.ok) return null;
948
+ return {
949
+ size: Number(response.headers.get("content-length") || 0),
950
+ mtime: new Date(response.headers.get("last-modified") || Date.now()),
951
+ format: "json"
952
+ };
953
+ }
954
+ async list(type) {
955
+ const items = await this.loadMany(type);
956
+ return items.map((i) => i.name);
957
+ }
958
+ async save(type, name, data, _options) {
959
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
960
+ method: "PUT",
961
+ headers: this.headers,
962
+ body: JSON.stringify(data)
963
+ });
964
+ if (!response.ok) {
965
+ throw new Error(`Remote save failed: ${response.statusText}`);
966
+ }
967
+ return {
968
+ success: true,
969
+ path: `${this.baseUrl}/${type}/${name}`,
970
+ saveTime: 0
971
+ };
972
+ }
973
+ };
974
+
975
+ // src/migration/index.ts
976
+ var migration_exports = {};
977
+ __export(migration_exports, {
978
+ MigrationExecutor: () => MigrationExecutor
979
+ });
980
+
981
+ // src/migration/executor.ts
982
+ var MigrationExecutor = class {
983
+ constructor(driver) {
984
+ this.driver = driver;
985
+ }
986
+ async executeChangeSet(changeSet) {
987
+ console.log(`Executing ChangeSet: ${changeSet.name} (${changeSet.id})`);
988
+ for (const op of changeSet.operations) {
989
+ try {
990
+ await this.executeOperation(op);
991
+ } catch (e) {
992
+ console.error(`Failed to execute operation ${op.type}:`, e);
993
+ throw e;
994
+ }
995
+ }
996
+ }
997
+ async executeOperation(op) {
998
+ switch (op.type) {
999
+ case "create_object":
1000
+ console.log(` > Create Object: ${op.object.name}`);
1001
+ await this.driver.createCollection(op.object.name, op.object);
1002
+ break;
1003
+ case "add_field":
1004
+ console.log(` > Add Field: ${op.objectName}.${op.fieldName}`);
1005
+ await this.driver.addColumn(op.objectName, op.fieldName, op.field);
1006
+ break;
1007
+ case "remove_field":
1008
+ console.log(` > Remove Field: ${op.objectName}.${op.fieldName}`);
1009
+ await this.driver.dropColumn(op.objectName, op.fieldName);
1010
+ break;
1011
+ case "delete_object":
1012
+ console.log(` > Delete Object: ${op.objectName}`);
1013
+ await this.driver.dropCollection(op.objectName);
1014
+ break;
1015
+ case "execute_sql":
1016
+ console.log(` > Execute SQL`);
1017
+ await this.driver.executeRaw(op.sql);
1018
+ break;
1019
+ case "modify_field":
1020
+ console.warn(` ! Modify Field: ${op.objectName}.${op.fieldName} (Not fully implemented)`);
1021
+ break;
1022
+ case "rename_object":
1023
+ console.warn(` ! Rename Object: ${op.oldName} -> ${op.newName} (Not fully implemented)`);
1024
+ break;
1025
+ default:
1026
+ throw new Error(`Unknown operation type`);
1027
+ }
1028
+ }
1029
+ };
1030
+ export {
1031
+ JSONSerializer,
1032
+ MemoryLoader,
1033
+ MetadataManager,
1034
+ MetadataPlugin,
1035
+ migration_exports as Migration,
1036
+ RemoteLoader,
1037
+ TypeScriptSerializer,
1038
+ YAMLSerializer
1039
+ };
1040
+ //# sourceMappingURL=index.mjs.map