@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
package/dist/node.cjs ADDED
@@ -0,0 +1,2201 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/node.ts
31
+ var node_exports = {};
32
+ __export(node_exports, {
33
+ DatabaseLoader: () => DatabaseLoader,
34
+ FilesystemLoader: () => FilesystemLoader,
35
+ JSONSerializer: () => JSONSerializer,
36
+ MemoryLoader: () => MemoryLoader,
37
+ MetadataManager: () => MetadataManager,
38
+ MetadataPlugin: () => MetadataPlugin,
39
+ Migration: () => migration_exports,
40
+ NodeMetadataManager: () => NodeMetadataManager,
41
+ RemoteLoader: () => RemoteLoader,
42
+ SysMetadataObject: () => SysMetadataObject,
43
+ TypeScriptSerializer: () => TypeScriptSerializer,
44
+ YAMLSerializer: () => YAMLSerializer
45
+ });
46
+ module.exports = __toCommonJS(node_exports);
47
+
48
+ // src/metadata-manager.ts
49
+ var import_core = require("@objectstack/core");
50
+
51
+ // src/serializers/json-serializer.ts
52
+ var JSONSerializer = class {
53
+ serialize(item, options) {
54
+ const { prettify = true, indent = 2, sortKeys = false } = options || {};
55
+ if (sortKeys) {
56
+ const sorted = this.sortObjectKeys(item);
57
+ return prettify ? JSON.stringify(sorted, null, indent) : JSON.stringify(sorted);
58
+ }
59
+ return prettify ? JSON.stringify(item, null, indent) : JSON.stringify(item);
60
+ }
61
+ deserialize(content, schema) {
62
+ const parsed = JSON.parse(content);
63
+ if (schema) {
64
+ return schema.parse(parsed);
65
+ }
66
+ return parsed;
67
+ }
68
+ getExtension() {
69
+ return ".json";
70
+ }
71
+ canHandle(format) {
72
+ return format === "json";
73
+ }
74
+ getFormat() {
75
+ return "json";
76
+ }
77
+ /**
78
+ * Recursively sort object keys
79
+ */
80
+ sortObjectKeys(obj) {
81
+ if (obj === null || typeof obj !== "object") {
82
+ return obj;
83
+ }
84
+ if (Array.isArray(obj)) {
85
+ return obj.map((item) => this.sortObjectKeys(item));
86
+ }
87
+ const sorted = {};
88
+ const keys = Object.keys(obj).sort();
89
+ for (const key of keys) {
90
+ sorted[key] = this.sortObjectKeys(obj[key]);
91
+ }
92
+ return sorted;
93
+ }
94
+ };
95
+
96
+ // src/serializers/yaml-serializer.ts
97
+ var yaml = __toESM(require("js-yaml"), 1);
98
+ var YAMLSerializer = class {
99
+ serialize(item, options) {
100
+ const { indent = 2, sortKeys = false } = options || {};
101
+ return yaml.dump(item, {
102
+ indent,
103
+ sortKeys,
104
+ lineWidth: -1,
105
+ // Disable line wrapping
106
+ noRefs: true
107
+ // Disable YAML references
108
+ });
109
+ }
110
+ deserialize(content, schema) {
111
+ const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
112
+ if (schema) {
113
+ return schema.parse(parsed);
114
+ }
115
+ return parsed;
116
+ }
117
+ getExtension() {
118
+ return ".yaml";
119
+ }
120
+ canHandle(format) {
121
+ return format === "yaml";
122
+ }
123
+ getFormat() {
124
+ return "yaml";
125
+ }
126
+ };
127
+
128
+ // src/serializers/typescript-serializer.ts
129
+ var TypeScriptSerializer = class {
130
+ constructor(format = "typescript") {
131
+ this.format = format;
132
+ }
133
+ serialize(item, options) {
134
+ const { prettify = true, indent = 2 } = options || {};
135
+ const jsonStr = JSON.stringify(item, null, prettify ? indent : 0);
136
+ if (this.format === "typescript") {
137
+ return `import type { ServiceObject } from '@objectstack/spec/data';
138
+
139
+ export const metadata: ServiceObject = ${jsonStr};
140
+
141
+ export default metadata;
142
+ `;
143
+ } else {
144
+ return `export const metadata = ${jsonStr};
145
+
146
+ export default metadata;
147
+ `;
148
+ }
149
+ }
150
+ deserialize(content, schema) {
151
+ let objectStart = content.indexOf("export const");
152
+ if (objectStart === -1) {
153
+ objectStart = content.indexOf("export default");
154
+ }
155
+ if (objectStart === -1) {
156
+ throw new Error(
157
+ 'Could not parse TypeScript/JavaScript module. Expected export pattern: "export const metadata = {...};" or "export default {...};"'
158
+ );
159
+ }
160
+ const braceStart = content.indexOf("{", objectStart);
161
+ if (braceStart === -1) {
162
+ throw new Error("Could not find object literal in export statement");
163
+ }
164
+ let braceCount = 0;
165
+ let braceEnd = -1;
166
+ let inString = false;
167
+ let stringChar = "";
168
+ for (let i = braceStart; i < content.length; i++) {
169
+ const char = content[i];
170
+ const prevChar = i > 0 ? content[i - 1] : "";
171
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
172
+ if (!inString) {
173
+ inString = true;
174
+ stringChar = char;
175
+ } else if (char === stringChar) {
176
+ inString = false;
177
+ stringChar = "";
178
+ }
179
+ }
180
+ if (!inString) {
181
+ if (char === "{") braceCount++;
182
+ if (char === "}") {
183
+ braceCount--;
184
+ if (braceCount === 0) {
185
+ braceEnd = i;
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ if (braceEnd === -1) {
192
+ throw new Error("Could not find matching closing brace for object literal");
193
+ }
194
+ const objectLiteral = content.substring(braceStart, braceEnd + 1);
195
+ try {
196
+ const parsed = JSON.parse(objectLiteral);
197
+ if (schema) {
198
+ return schema.parse(parsed);
199
+ }
200
+ return parsed;
201
+ } catch (error) {
202
+ throw new Error(
203
+ `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).`
204
+ );
205
+ }
206
+ }
207
+ getExtension() {
208
+ return this.format === "typescript" ? ".ts" : ".js";
209
+ }
210
+ canHandle(format) {
211
+ return format === "typescript" || format === "javascript";
212
+ }
213
+ getFormat() {
214
+ return this.format;
215
+ }
216
+ };
217
+
218
+ // src/objects/sys-metadata.object.ts
219
+ var import_data = require("@objectstack/spec/data");
220
+ var SysMetadataObject = import_data.ObjectSchema.create({
221
+ namespace: "sys",
222
+ name: "metadata",
223
+ label: "System Metadata",
224
+ pluralLabel: "System Metadata",
225
+ icon: "settings",
226
+ isSystem: true,
227
+ description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
228
+ fields: {
229
+ /** Primary Key (UUID) */
230
+ id: import_data.Field.text({
231
+ label: "ID",
232
+ required: true,
233
+ readonly: true
234
+ }),
235
+ /** Machine name — unique identifier used in code references */
236
+ name: import_data.Field.text({
237
+ label: "Name",
238
+ required: true,
239
+ searchable: true,
240
+ maxLength: 255
241
+ }),
242
+ /** Metadata type (e.g. "object", "view", "flow") */
243
+ type: import_data.Field.text({
244
+ label: "Metadata Type",
245
+ required: true,
246
+ searchable: true,
247
+ maxLength: 100
248
+ }),
249
+ /** Namespace / module grouping (e.g. "crm", "core") */
250
+ namespace: import_data.Field.text({
251
+ label: "Namespace",
252
+ required: false,
253
+ defaultValue: "default",
254
+ maxLength: 100
255
+ }),
256
+ /** Package that owns/delivered this metadata */
257
+ package_id: import_data.Field.text({
258
+ label: "Package ID",
259
+ required: false,
260
+ maxLength: 255
261
+ }),
262
+ /** Who manages this record: package, platform, or user */
263
+ managed_by: import_data.Field.select(["package", "platform", "user"], {
264
+ label: "Managed By",
265
+ required: false
266
+ }),
267
+ /** Scope: system (code), platform (admin DB), user (personal DB) */
268
+ scope: import_data.Field.select(["system", "platform", "user"], {
269
+ label: "Scope",
270
+ required: true,
271
+ defaultValue: "platform"
272
+ }),
273
+ /** JSON payload — the actual metadata configuration */
274
+ metadata: import_data.Field.textarea({
275
+ label: "Metadata",
276
+ required: true,
277
+ description: "JSON-serialized metadata payload"
278
+ }),
279
+ /** Parent metadata name for extension/override */
280
+ extends: import_data.Field.text({
281
+ label: "Extends",
282
+ required: false,
283
+ maxLength: 255
284
+ }),
285
+ /** Merge strategy when extending parent metadata */
286
+ strategy: import_data.Field.select(["merge", "replace"], {
287
+ label: "Strategy",
288
+ required: false,
289
+ defaultValue: "merge"
290
+ }),
291
+ /** Owner user ID (for user-scope items) */
292
+ owner: import_data.Field.text({
293
+ label: "Owner",
294
+ required: false,
295
+ maxLength: 255
296
+ }),
297
+ /** Lifecycle state */
298
+ state: import_data.Field.select(["draft", "active", "archived", "deprecated"], {
299
+ label: "State",
300
+ required: false,
301
+ defaultValue: "active"
302
+ }),
303
+ /** Tenant ID for multi-tenant isolation */
304
+ tenant_id: import_data.Field.text({
305
+ label: "Tenant ID",
306
+ required: false,
307
+ maxLength: 255
308
+ }),
309
+ /** Version number for optimistic concurrency */
310
+ version: import_data.Field.number({
311
+ label: "Version",
312
+ required: false,
313
+ defaultValue: 1
314
+ }),
315
+ /** Content checksum for change detection */
316
+ checksum: import_data.Field.text({
317
+ label: "Checksum",
318
+ required: false,
319
+ maxLength: 64
320
+ }),
321
+ /** Origin of this metadata record */
322
+ source: import_data.Field.select(["filesystem", "database", "api", "migration"], {
323
+ label: "Source",
324
+ required: false
325
+ }),
326
+ /** Classification tags (JSON array) */
327
+ tags: import_data.Field.textarea({
328
+ label: "Tags",
329
+ required: false,
330
+ description: "JSON-serialized array of classification tags"
331
+ }),
332
+ /** Audit fields */
333
+ created_by: import_data.Field.text({
334
+ label: "Created By",
335
+ required: false,
336
+ readonly: true,
337
+ maxLength: 255
338
+ }),
339
+ created_at: import_data.Field.datetime({
340
+ label: "Created At",
341
+ required: false,
342
+ readonly: true
343
+ }),
344
+ updated_by: import_data.Field.text({
345
+ label: "Updated By",
346
+ required: false,
347
+ maxLength: 255
348
+ }),
349
+ updated_at: import_data.Field.datetime({
350
+ label: "Updated At",
351
+ required: false
352
+ })
353
+ },
354
+ indexes: [
355
+ { fields: ["type", "name"], unique: true },
356
+ { fields: ["type", "scope"] },
357
+ { fields: ["tenant_id"] },
358
+ { fields: ["state"] },
359
+ { fields: ["namespace"] }
360
+ ],
361
+ enable: {
362
+ trackHistory: true,
363
+ searchable: false,
364
+ apiEnabled: true,
365
+ apiMethods: ["get", "list", "create", "update", "delete"],
366
+ trash: false
367
+ }
368
+ });
369
+
370
+ // src/loaders/database-loader.ts
371
+ var DatabaseLoader = class {
372
+ constructor(options) {
373
+ this.contract = {
374
+ name: "database",
375
+ protocol: "datasource:",
376
+ capabilities: {
377
+ read: true,
378
+ write: true,
379
+ watch: false,
380
+ list: true
381
+ }
382
+ };
383
+ this.schemaReady = false;
384
+ this.driver = options.driver;
385
+ this.tableName = options.tableName ?? "sys_metadata";
386
+ this.tenantId = options.tenantId;
387
+ }
388
+ /**
389
+ * Ensure the metadata table exists.
390
+ * Uses IDataDriver.syncSchema with the SysMetadataObject definition
391
+ * to idempotently create/update the table.
392
+ */
393
+ async ensureSchema() {
394
+ if (this.schemaReady) return;
395
+ try {
396
+ await this.driver.syncSchema(this.tableName, {
397
+ ...SysMetadataObject,
398
+ name: this.tableName
399
+ });
400
+ this.schemaReady = true;
401
+ } catch {
402
+ this.schemaReady = true;
403
+ }
404
+ }
405
+ /**
406
+ * Build base filter conditions for queries.
407
+ * Always includes tenantId when configured.
408
+ */
409
+ baseFilter(type, name) {
410
+ const filter = { type };
411
+ if (name !== void 0) {
412
+ filter.name = name;
413
+ }
414
+ if (this.tenantId) {
415
+ filter.tenant_id = this.tenantId;
416
+ }
417
+ return filter;
418
+ }
419
+ /**
420
+ * Convert a database row to a metadata payload.
421
+ * Parses the JSON `metadata` column back into an object.
422
+ */
423
+ rowToData(row) {
424
+ if (!row || !row.metadata) return null;
425
+ const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
426
+ return payload;
427
+ }
428
+ /**
429
+ * Convert a database row to a MetadataRecord-like object.
430
+ */
431
+ rowToRecord(row) {
432
+ return {
433
+ id: row.id,
434
+ name: row.name,
435
+ type: row.type,
436
+ namespace: row.namespace ?? "default",
437
+ packageId: row.package_id,
438
+ managedBy: row.managed_by,
439
+ scope: row.scope ?? "platform",
440
+ metadata: this.rowToData(row) ?? {},
441
+ extends: row.extends,
442
+ strategy: row.strategy ?? "merge",
443
+ owner: row.owner,
444
+ state: row.state ?? "active",
445
+ tenantId: row.tenant_id,
446
+ version: row.version ?? 1,
447
+ checksum: row.checksum,
448
+ source: row.source,
449
+ tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
450
+ createdBy: row.created_by,
451
+ createdAt: row.created_at,
452
+ updatedBy: row.updated_by,
453
+ updatedAt: row.updated_at
454
+ };
455
+ }
456
+ // ==========================================
457
+ // MetadataLoader Interface Implementation
458
+ // ==========================================
459
+ async load(type, name, _options) {
460
+ const startTime = Date.now();
461
+ await this.ensureSchema();
462
+ try {
463
+ const row = await this.driver.findOne(this.tableName, {
464
+ object: this.tableName,
465
+ where: this.baseFilter(type, name)
466
+ });
467
+ if (!row) {
468
+ return {
469
+ data: null,
470
+ loadTime: Date.now() - startTime
471
+ };
472
+ }
473
+ const data = this.rowToData(row);
474
+ const record = this.rowToRecord(row);
475
+ return {
476
+ data,
477
+ source: "database",
478
+ format: "json",
479
+ etag: record.checksum,
480
+ loadTime: Date.now() - startTime
481
+ };
482
+ } catch {
483
+ return {
484
+ data: null,
485
+ loadTime: Date.now() - startTime
486
+ };
487
+ }
488
+ }
489
+ async loadMany(type, _options) {
490
+ await this.ensureSchema();
491
+ try {
492
+ const rows = await this.driver.find(this.tableName, {
493
+ object: this.tableName,
494
+ where: this.baseFilter(type)
495
+ });
496
+ return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
497
+ } catch {
498
+ return [];
499
+ }
500
+ }
501
+ async exists(type, name) {
502
+ await this.ensureSchema();
503
+ try {
504
+ const count = await this.driver.count(this.tableName, {
505
+ object: this.tableName,
506
+ where: this.baseFilter(type, name)
507
+ });
508
+ return count > 0;
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+ async stat(type, name) {
514
+ await this.ensureSchema();
515
+ try {
516
+ const row = await this.driver.findOne(this.tableName, {
517
+ object: this.tableName,
518
+ where: this.baseFilter(type, name)
519
+ });
520
+ if (!row) return null;
521
+ const record = this.rowToRecord(row);
522
+ const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
523
+ return {
524
+ size: metadataStr.length,
525
+ mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
526
+ format: "json",
527
+ etag: record.checksum
528
+ };
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+ async list(type) {
534
+ await this.ensureSchema();
535
+ try {
536
+ const rows = await this.driver.find(this.tableName, {
537
+ object: this.tableName,
538
+ where: this.baseFilter(type),
539
+ fields: ["name"]
540
+ });
541
+ return rows.map((row) => row.name).filter((name) => typeof name === "string");
542
+ } catch {
543
+ return [];
544
+ }
545
+ }
546
+ async save(type, name, data, _options) {
547
+ const startTime = Date.now();
548
+ await this.ensureSchema();
549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
550
+ const metadataJson = JSON.stringify(data);
551
+ try {
552
+ const existing = await this.driver.findOne(this.tableName, {
553
+ object: this.tableName,
554
+ where: this.baseFilter(type, name)
555
+ });
556
+ if (existing) {
557
+ const version = (existing.version ?? 0) + 1;
558
+ await this.driver.update(this.tableName, existing.id, {
559
+ metadata: metadataJson,
560
+ version,
561
+ updated_at: now,
562
+ state: "active"
563
+ });
564
+ return {
565
+ success: true,
566
+ path: `datasource://${this.tableName}/${type}/${name}`,
567
+ size: metadataJson.length,
568
+ saveTime: Date.now() - startTime
569
+ };
570
+ } else {
571
+ const id = generateId();
572
+ await this.driver.create(this.tableName, {
573
+ id,
574
+ name,
575
+ type,
576
+ namespace: "default",
577
+ scope: data?.scope ?? "platform",
578
+ metadata: metadataJson,
579
+ strategy: "merge",
580
+ state: "active",
581
+ version: 1,
582
+ source: "database",
583
+ ...this.tenantId ? { tenant_id: this.tenantId } : {},
584
+ created_at: now,
585
+ updated_at: now
586
+ });
587
+ return {
588
+ success: true,
589
+ path: `datasource://${this.tableName}/${type}/${name}`,
590
+ size: metadataJson.length,
591
+ saveTime: Date.now() - startTime
592
+ };
593
+ }
594
+ } catch (error) {
595
+ throw new Error(
596
+ `DatabaseLoader save failed for ${type}/${name}: ${error instanceof Error ? error.message : String(error)}`
597
+ );
598
+ }
599
+ }
600
+ };
601
+ function generateId() {
602
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
603
+ return globalThis.crypto.randomUUID();
604
+ }
605
+ return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
606
+ }
607
+
608
+ // src/metadata-manager.ts
609
+ var MetadataManager = class {
610
+ constructor(config) {
611
+ this.loaders = /* @__PURE__ */ new Map();
612
+ this.watchCallbacks = /* @__PURE__ */ new Map();
613
+ // In-memory metadata registry: type -> name -> data
614
+ this.registry = /* @__PURE__ */ new Map();
615
+ // Overlay storage: "type:name:scope" -> MetadataOverlay
616
+ this.overlays = /* @__PURE__ */ new Map();
617
+ // Type registry for metadata type info
618
+ this.typeRegistry = [];
619
+ // Dependency tracking: "type:name" -> dependencies
620
+ this.dependencies = /* @__PURE__ */ new Map();
621
+ this.config = config;
622
+ this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
623
+ this.serializers = /* @__PURE__ */ new Map();
624
+ const formats = config.formats || ["typescript", "json", "yaml"];
625
+ if (formats.includes("json")) {
626
+ this.serializers.set("json", new JSONSerializer());
627
+ }
628
+ if (formats.includes("yaml")) {
629
+ this.serializers.set("yaml", new YAMLSerializer());
630
+ }
631
+ if (formats.includes("typescript")) {
632
+ this.serializers.set("typescript", new TypeScriptSerializer("typescript"));
633
+ }
634
+ if (formats.includes("javascript")) {
635
+ this.serializers.set("javascript", new TypeScriptSerializer("javascript"));
636
+ }
637
+ if (config.loaders && config.loaders.length > 0) {
638
+ config.loaders.forEach((loader) => this.registerLoader(loader));
639
+ }
640
+ if (config.datasource && config.driver) {
641
+ this.setDatabaseDriver(config.driver);
642
+ }
643
+ }
644
+ /**
645
+ * Set the type registry for metadata type discovery.
646
+ */
647
+ setTypeRegistry(entries) {
648
+ this.typeRegistry = entries;
649
+ }
650
+ /**
651
+ * Configure and register a DatabaseLoader for database-backed metadata persistence.
652
+ * Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
653
+ *
654
+ * @param driver - An IDataDriver instance for database operations
655
+ */
656
+ setDatabaseDriver(driver) {
657
+ const tableName = this.config.tableName ?? "sys_metadata";
658
+ const dbLoader = new DatabaseLoader({
659
+ driver,
660
+ tableName
661
+ });
662
+ this.registerLoader(dbLoader);
663
+ this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
664
+ }
665
+ /**
666
+ * Register a new metadata loader (data source)
667
+ */
668
+ registerLoader(loader) {
669
+ this.loaders.set(loader.contract.name, loader);
670
+ this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
671
+ }
672
+ // ==========================================
673
+ // IMetadataService — Core CRUD Operations
674
+ // ==========================================
675
+ /**
676
+ * Register/save a metadata item by type
677
+ */
678
+ async register(type, name, data) {
679
+ if (!this.registry.has(type)) {
680
+ this.registry.set(type, /* @__PURE__ */ new Map());
681
+ }
682
+ this.registry.get(type).set(name, data);
683
+ }
684
+ /**
685
+ * Get a metadata item by type and name.
686
+ * Checks in-memory registry first, then falls back to loaders.
687
+ */
688
+ async get(type, name) {
689
+ const typeStore = this.registry.get(type);
690
+ if (typeStore?.has(name)) {
691
+ return typeStore.get(name);
692
+ }
693
+ const result = await this.load(type, name);
694
+ return result ?? void 0;
695
+ }
696
+ /**
697
+ * List all metadata items of a given type
698
+ */
699
+ async list(type) {
700
+ const items = /* @__PURE__ */ new Map();
701
+ const typeStore = this.registry.get(type);
702
+ if (typeStore) {
703
+ for (const [name, data] of typeStore) {
704
+ items.set(name, data);
705
+ }
706
+ }
707
+ for (const loader of this.loaders.values()) {
708
+ try {
709
+ const loaderItems = await loader.loadMany(type);
710
+ for (const item of loaderItems) {
711
+ const itemAny = item;
712
+ if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
713
+ items.set(itemAny.name, item);
714
+ }
715
+ }
716
+ } catch (e) {
717
+ this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
718
+ }
719
+ }
720
+ return Array.from(items.values());
721
+ }
722
+ /**
723
+ * Unregister/remove a metadata item by type and name
724
+ */
725
+ async unregister(type, name) {
726
+ const typeStore = this.registry.get(type);
727
+ if (typeStore) {
728
+ typeStore.delete(name);
729
+ if (typeStore.size === 0) {
730
+ this.registry.delete(type);
731
+ }
732
+ }
733
+ }
734
+ /**
735
+ * Check if a metadata item exists
736
+ */
737
+ async exists(type, name) {
738
+ if (this.registry.get(type)?.has(name)) {
739
+ return true;
740
+ }
741
+ for (const loader of this.loaders.values()) {
742
+ if (await loader.exists(type, name)) {
743
+ return true;
744
+ }
745
+ }
746
+ return false;
747
+ }
748
+ /**
749
+ * List all names of metadata items of a given type
750
+ */
751
+ async listNames(type) {
752
+ const names = /* @__PURE__ */ new Set();
753
+ const typeStore = this.registry.get(type);
754
+ if (typeStore) {
755
+ for (const name of typeStore.keys()) {
756
+ names.add(name);
757
+ }
758
+ }
759
+ for (const loader of this.loaders.values()) {
760
+ const result = await loader.list(type);
761
+ result.forEach((item) => names.add(item));
762
+ }
763
+ return Array.from(names);
764
+ }
765
+ /**
766
+ * Convenience: get an object definition by name
767
+ */
768
+ async getObject(name) {
769
+ return this.get("object", name);
770
+ }
771
+ /**
772
+ * Convenience: list all object definitions
773
+ */
774
+ async listObjects() {
775
+ return this.list("object");
776
+ }
777
+ // ==========================================
778
+ // Convenience: UI Metadata
779
+ // ==========================================
780
+ /**
781
+ * Convenience: get a view definition by name
782
+ */
783
+ async getView(name) {
784
+ return this.get("view", name);
785
+ }
786
+ /**
787
+ * Convenience: list view definitions, optionally filtered by object
788
+ */
789
+ async listViews(object) {
790
+ const views = await this.list("view");
791
+ if (object) {
792
+ return views.filter((v) => v?.object === object);
793
+ }
794
+ return views;
795
+ }
796
+ /**
797
+ * Convenience: get a dashboard definition by name
798
+ */
799
+ async getDashboard(name) {
800
+ return this.get("dashboard", name);
801
+ }
802
+ /**
803
+ * Convenience: list all dashboard definitions
804
+ */
805
+ async listDashboards() {
806
+ return this.list("dashboard");
807
+ }
808
+ // ==========================================
809
+ // Package Management
810
+ // ==========================================
811
+ /**
812
+ * Unregister all metadata items from a specific package
813
+ */
814
+ async unregisterPackage(packageName) {
815
+ for (const [type, typeStore] of this.registry) {
816
+ const toDelete = [];
817
+ for (const [name, data] of typeStore) {
818
+ const meta = data;
819
+ if (meta?.packageId === packageName || meta?.package === packageName) {
820
+ toDelete.push(name);
821
+ }
822
+ }
823
+ for (const name of toDelete) {
824
+ typeStore.delete(name);
825
+ }
826
+ if (typeStore.size === 0) {
827
+ this.registry.delete(type);
828
+ }
829
+ }
830
+ }
831
+ /**
832
+ * Publish an entire package:
833
+ * 1. Validate all draft items
834
+ * 2. Snapshot all items in the package (publishedDefinition = clone(metadata))
835
+ * 3. Increment version
836
+ * 4. Set all items state → active
837
+ */
838
+ async publishPackage(packageId, options) {
839
+ const now = (/* @__PURE__ */ new Date()).toISOString();
840
+ const shouldValidate = options?.validate !== false;
841
+ const publishedBy = options?.publishedBy;
842
+ const packageItems = [];
843
+ for (const [type, typeStore] of this.registry) {
844
+ for (const [name, data] of typeStore) {
845
+ const meta = data;
846
+ if (meta?.packageId === packageId || meta?.package === packageId) {
847
+ packageItems.push({ type, name, data: meta });
848
+ }
849
+ }
850
+ }
851
+ if (packageItems.length === 0) {
852
+ return {
853
+ success: false,
854
+ packageId,
855
+ version: 0,
856
+ publishedAt: now,
857
+ itemsPublished: 0,
858
+ validationErrors: [{ type: "", name: "", message: `No metadata items found for package '${packageId}'` }]
859
+ };
860
+ }
861
+ if (shouldValidate) {
862
+ const validationErrors = [];
863
+ for (const item of packageItems) {
864
+ const result = await this.validate(item.type, item.data);
865
+ if (!result.valid && result.errors) {
866
+ for (const err of result.errors) {
867
+ validationErrors.push({
868
+ type: item.type,
869
+ name: item.name,
870
+ message: err.message
871
+ });
872
+ }
873
+ }
874
+ }
875
+ const packageItemKeys = new Set(packageItems.map((i) => `${i.type}:${i.name}`));
876
+ for (const item of packageItems) {
877
+ const deps = await this.getDependencies(item.type, item.name);
878
+ for (const dep of deps) {
879
+ const depKey = `${dep.targetType}:${dep.targetName}`;
880
+ if (packageItemKeys.has(depKey)) continue;
881
+ const depItem = await this.get(dep.targetType, dep.targetName);
882
+ if (!depItem) {
883
+ validationErrors.push({
884
+ type: item.type,
885
+ name: item.name,
886
+ message: `Dependency '${dep.targetType}:${dep.targetName}' not found`
887
+ });
888
+ } else {
889
+ const depMeta = depItem;
890
+ if (depMeta.publishedDefinition === void 0 && depMeta.state !== "active") {
891
+ validationErrors.push({
892
+ type: item.type,
893
+ name: item.name,
894
+ message: `Dependency '${dep.targetType}:${dep.targetName}' is not published`
895
+ });
896
+ }
897
+ }
898
+ }
899
+ }
900
+ if (validationErrors.length > 0) {
901
+ return {
902
+ success: false,
903
+ packageId,
904
+ version: 0,
905
+ publishedAt: now,
906
+ itemsPublished: 0,
907
+ validationErrors
908
+ };
909
+ }
910
+ }
911
+ let maxVersion = 0;
912
+ for (const item of packageItems) {
913
+ const v = typeof item.data.version === "number" ? item.data.version : 0;
914
+ if (v > maxVersion) maxVersion = v;
915
+ }
916
+ const newVersion = maxVersion + 1;
917
+ for (const item of packageItems) {
918
+ const updated = {
919
+ ...item.data,
920
+ publishedDefinition: structuredClone(item.data.metadata ?? item.data),
921
+ publishedAt: now,
922
+ publishedBy: publishedBy ?? item.data.publishedBy,
923
+ version: newVersion,
924
+ state: "active"
925
+ };
926
+ await this.register(item.type, item.name, updated);
927
+ }
928
+ return {
929
+ success: true,
930
+ packageId,
931
+ version: newVersion,
932
+ publishedAt: now,
933
+ itemsPublished: packageItems.length
934
+ };
935
+ }
936
+ /**
937
+ * Revert entire package to last published state.
938
+ * Restores all metadata definitions from their published snapshots.
939
+ */
940
+ async revertPackage(packageId) {
941
+ const packageItems = [];
942
+ for (const [type, typeStore] of this.registry) {
943
+ for (const [name, data] of typeStore) {
944
+ const meta = data;
945
+ if (meta?.packageId === packageId || meta?.package === packageId) {
946
+ packageItems.push({ type, name, data: meta });
947
+ }
948
+ }
949
+ }
950
+ if (packageItems.length === 0) {
951
+ throw new Error(`No metadata items found for package '${packageId}'`);
952
+ }
953
+ const hasPublished = packageItems.some((item) => item.data.publishedDefinition !== void 0);
954
+ if (!hasPublished) {
955
+ throw new Error(`Package '${packageId}' has never been published`);
956
+ }
957
+ for (const item of packageItems) {
958
+ if (item.data.publishedDefinition !== void 0) {
959
+ const reverted = {
960
+ ...item.data,
961
+ metadata: structuredClone(item.data.publishedDefinition),
962
+ state: "active"
963
+ };
964
+ await this.register(item.type, item.name, reverted);
965
+ }
966
+ }
967
+ }
968
+ /**
969
+ * Get the published version of any metadata item (for runtime serving).
970
+ * Returns publishedDefinition if exists, else current definition.
971
+ */
972
+ async getPublished(type, name) {
973
+ const item = await this.get(type, name);
974
+ if (!item) return void 0;
975
+ const meta = item;
976
+ if (meta.publishedDefinition !== void 0) {
977
+ return meta.publishedDefinition;
978
+ }
979
+ return meta.metadata ?? item;
980
+ }
981
+ // ==========================================
982
+ // Query / Search
983
+ // ==========================================
984
+ /**
985
+ * Query metadata items with filtering, sorting, and pagination
986
+ */
987
+ async query(query) {
988
+ const { types, search, page = 1, pageSize = 50, sortBy = "name", sortOrder = "asc" } = query;
989
+ const allItems = [];
990
+ const targetTypes = types && types.length > 0 ? types : Array.from(this.registry.keys());
991
+ for (const type of targetTypes) {
992
+ const items = await this.list(type);
993
+ for (const item of items) {
994
+ const meta = item;
995
+ allItems.push({
996
+ type,
997
+ name: meta?.name ?? "",
998
+ namespace: meta?.namespace,
999
+ label: meta?.label,
1000
+ scope: meta?.scope,
1001
+ state: meta?.state,
1002
+ packageId: meta?.packageId,
1003
+ updatedAt: meta?.updatedAt
1004
+ });
1005
+ }
1006
+ }
1007
+ let filtered = allItems;
1008
+ if (search) {
1009
+ const searchLower = search.toLowerCase();
1010
+ filtered = filtered.filter(
1011
+ (item) => item.name.toLowerCase().includes(searchLower) || item.label && item.label.toLowerCase().includes(searchLower)
1012
+ );
1013
+ }
1014
+ if (query.scope) {
1015
+ filtered = filtered.filter((item) => item.scope === query.scope);
1016
+ }
1017
+ if (query.state) {
1018
+ filtered = filtered.filter((item) => item.state === query.state);
1019
+ }
1020
+ if (query.namespaces && query.namespaces.length > 0) {
1021
+ filtered = filtered.filter((item) => item.namespace && query.namespaces.includes(item.namespace));
1022
+ }
1023
+ if (query.packageId) {
1024
+ filtered = filtered.filter((item) => item.packageId === query.packageId);
1025
+ }
1026
+ if (query.tags && query.tags.length > 0) {
1027
+ filtered = filtered.filter((item) => {
1028
+ const meta = item;
1029
+ return meta?.tags && query.tags.some((t) => meta.tags.includes(t));
1030
+ });
1031
+ }
1032
+ filtered.sort((a, b) => {
1033
+ const aVal = a[sortBy] ?? "";
1034
+ const bVal = b[sortBy] ?? "";
1035
+ const cmp = String(aVal).localeCompare(String(bVal));
1036
+ return sortOrder === "desc" ? -cmp : cmp;
1037
+ });
1038
+ const total = filtered.length;
1039
+ const start = (page - 1) * pageSize;
1040
+ const paged = filtered.slice(start, start + pageSize);
1041
+ return {
1042
+ items: paged,
1043
+ total,
1044
+ page,
1045
+ pageSize
1046
+ };
1047
+ }
1048
+ // ==========================================
1049
+ // Bulk Operations
1050
+ // ==========================================
1051
+ /**
1052
+ * Register multiple metadata items in a single batch
1053
+ */
1054
+ async bulkRegister(items, options) {
1055
+ const { continueOnError = false } = options ?? {};
1056
+ let succeeded = 0;
1057
+ let failed = 0;
1058
+ const errors = [];
1059
+ for (const item of items) {
1060
+ try {
1061
+ await this.register(item.type, item.name, item.data);
1062
+ succeeded++;
1063
+ } catch (e) {
1064
+ failed++;
1065
+ errors.push({
1066
+ type: item.type,
1067
+ name: item.name,
1068
+ error: e instanceof Error ? e.message : String(e)
1069
+ });
1070
+ if (!continueOnError) break;
1071
+ }
1072
+ }
1073
+ return {
1074
+ total: items.length,
1075
+ succeeded,
1076
+ failed,
1077
+ errors: errors.length > 0 ? errors : void 0
1078
+ };
1079
+ }
1080
+ /**
1081
+ * Unregister multiple metadata items in a single batch
1082
+ */
1083
+ async bulkUnregister(items) {
1084
+ let succeeded = 0;
1085
+ let failed = 0;
1086
+ const errors = [];
1087
+ for (const item of items) {
1088
+ try {
1089
+ await this.unregister(item.type, item.name);
1090
+ succeeded++;
1091
+ } catch (e) {
1092
+ failed++;
1093
+ errors.push({
1094
+ type: item.type,
1095
+ name: item.name,
1096
+ error: e instanceof Error ? e.message : String(e)
1097
+ });
1098
+ }
1099
+ }
1100
+ return {
1101
+ total: items.length,
1102
+ succeeded,
1103
+ failed,
1104
+ errors: errors.length > 0 ? errors : void 0
1105
+ };
1106
+ }
1107
+ // ==========================================
1108
+ // Overlay / Customization Management
1109
+ // ==========================================
1110
+ overlayKey(type, name, scope = "platform") {
1111
+ return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
1112
+ }
1113
+ /**
1114
+ * Get the active overlay for a metadata item
1115
+ */
1116
+ async getOverlay(type, name, scope) {
1117
+ return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
1118
+ }
1119
+ /**
1120
+ * Save/update an overlay for a metadata item
1121
+ */
1122
+ async saveOverlay(overlay) {
1123
+ const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
1124
+ this.overlays.set(key, overlay);
1125
+ }
1126
+ /**
1127
+ * Remove an overlay, reverting to the base definition
1128
+ */
1129
+ async removeOverlay(type, name, scope) {
1130
+ this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
1131
+ }
1132
+ /**
1133
+ * Get the effective (merged) metadata after applying all overlays.
1134
+ * Resolution order: system ← merge(platform) ← merge(user)
1135
+ */
1136
+ async getEffective(type, name, context) {
1137
+ const base = await this.get(type, name);
1138
+ if (!base) return void 0;
1139
+ let effective = { ...base };
1140
+ const platformOverlay = await this.getOverlay(type, name, "platform");
1141
+ if (platformOverlay?.active && platformOverlay.patch) {
1142
+ effective = { ...effective, ...platformOverlay.patch };
1143
+ }
1144
+ if (context?.userId) {
1145
+ const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
1146
+ const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
1147
+ if (userOverlay?.active && userOverlay.patch) {
1148
+ if (!userOverlay.owner || userOverlay.owner === context.userId) {
1149
+ effective = { ...effective, ...userOverlay.patch };
1150
+ }
1151
+ }
1152
+ } else {
1153
+ const userOverlay = await this.getOverlay(type, name, "user");
1154
+ if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
1155
+ effective = { ...effective, ...userOverlay.patch };
1156
+ }
1157
+ }
1158
+ return effective;
1159
+ }
1160
+ // ==========================================
1161
+ // Watch / Subscribe (IMetadataService)
1162
+ // ==========================================
1163
+ /**
1164
+ * Watch for metadata changes (IMetadataService contract).
1165
+ * Returns a handle for unsubscribing.
1166
+ */
1167
+ watchService(type, callback) {
1168
+ const wrappedCallback = (event) => {
1169
+ const mappedType = event.type === "added" ? "registered" : event.type === "deleted" ? "unregistered" : "updated";
1170
+ callback({
1171
+ type: mappedType,
1172
+ metadataType: event.metadataType ?? type,
1173
+ name: event.name ?? "",
1174
+ data: event.data
1175
+ });
1176
+ };
1177
+ this.addWatchCallback(type, wrappedCallback);
1178
+ return {
1179
+ unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
1180
+ };
1181
+ }
1182
+ // ==========================================
1183
+ // Import / Export
1184
+ // ==========================================
1185
+ /**
1186
+ * Export metadata as a portable bundle
1187
+ */
1188
+ async exportMetadata(options) {
1189
+ const bundle = {};
1190
+ const targetTypes = options?.types ?? Array.from(this.registry.keys());
1191
+ for (const type of targetTypes) {
1192
+ const items = await this.list(type);
1193
+ if (items.length > 0) {
1194
+ bundle[type] = items;
1195
+ }
1196
+ }
1197
+ return bundle;
1198
+ }
1199
+ /**
1200
+ * Import metadata from a portable bundle
1201
+ */
1202
+ async importMetadata(data, options) {
1203
+ const {
1204
+ conflictResolution = "skip",
1205
+ validate: _validate = true,
1206
+ dryRun = false
1207
+ } = options ?? {};
1208
+ const bundle = data;
1209
+ let total = 0;
1210
+ let imported = 0;
1211
+ let skipped = 0;
1212
+ let failed = 0;
1213
+ const errors = [];
1214
+ for (const [type, items] of Object.entries(bundle)) {
1215
+ if (!Array.isArray(items)) continue;
1216
+ for (const item of items) {
1217
+ total++;
1218
+ const meta = item;
1219
+ const name = meta?.name;
1220
+ if (!name) {
1221
+ failed++;
1222
+ errors.push({ type, name: "(unknown)", error: "Item missing name field" });
1223
+ continue;
1224
+ }
1225
+ try {
1226
+ const itemExists = await this.exists(type, name);
1227
+ if (itemExists && conflictResolution === "skip") {
1228
+ skipped++;
1229
+ continue;
1230
+ }
1231
+ if (!dryRun) {
1232
+ if (itemExists && conflictResolution === "merge") {
1233
+ const existing = await this.get(type, name);
1234
+ const merged = { ...existing, ...item };
1235
+ await this.register(type, name, merged);
1236
+ } else {
1237
+ await this.register(type, name, item);
1238
+ }
1239
+ }
1240
+ imported++;
1241
+ } catch (e) {
1242
+ failed++;
1243
+ errors.push({
1244
+ type,
1245
+ name,
1246
+ error: e instanceof Error ? e.message : String(e)
1247
+ });
1248
+ }
1249
+ }
1250
+ }
1251
+ return {
1252
+ total,
1253
+ imported,
1254
+ skipped,
1255
+ failed,
1256
+ errors: errors.length > 0 ? errors : void 0
1257
+ };
1258
+ }
1259
+ // ==========================================
1260
+ // Validation
1261
+ // ==========================================
1262
+ /**
1263
+ * Validate a metadata item against its type schema.
1264
+ * Returns validation result with errors and warnings.
1265
+ */
1266
+ async validate(_type, data) {
1267
+ if (data === null || data === void 0) {
1268
+ return {
1269
+ valid: false,
1270
+ errors: [{ path: "", message: "Metadata data cannot be null or undefined" }]
1271
+ };
1272
+ }
1273
+ if (typeof data !== "object") {
1274
+ return {
1275
+ valid: false,
1276
+ errors: [{ path: "", message: "Metadata data must be an object" }]
1277
+ };
1278
+ }
1279
+ const meta = data;
1280
+ const warnings = [];
1281
+ if (!meta.name) {
1282
+ return {
1283
+ valid: false,
1284
+ errors: [{ path: "name", message: "Metadata item must have a name field" }]
1285
+ };
1286
+ }
1287
+ if (!meta.label) {
1288
+ warnings.push({ path: "label", message: "Missing label field (recommended)" });
1289
+ }
1290
+ return { valid: true, warnings: warnings.length > 0 ? warnings : void 0 };
1291
+ }
1292
+ // ==========================================
1293
+ // Type Registry
1294
+ // ==========================================
1295
+ /**
1296
+ * Get all registered metadata types
1297
+ */
1298
+ async getRegisteredTypes() {
1299
+ const types = /* @__PURE__ */ new Set();
1300
+ for (const entry of this.typeRegistry) {
1301
+ types.add(entry.type);
1302
+ }
1303
+ for (const type of this.registry.keys()) {
1304
+ types.add(type);
1305
+ }
1306
+ return Array.from(types);
1307
+ }
1308
+ /**
1309
+ * Get detailed information about a metadata type
1310
+ */
1311
+ async getTypeInfo(type) {
1312
+ const entry = this.typeRegistry.find((e) => e.type === type);
1313
+ if (!entry) return void 0;
1314
+ return {
1315
+ type: entry.type,
1316
+ label: entry.label,
1317
+ description: entry.description,
1318
+ filePatterns: entry.filePatterns,
1319
+ supportsOverlay: entry.supportsOverlay,
1320
+ domain: entry.domain
1321
+ };
1322
+ }
1323
+ // ==========================================
1324
+ // Dependency Tracking
1325
+ // ==========================================
1326
+ /**
1327
+ * Get metadata items that this item depends on
1328
+ */
1329
+ async getDependencies(type, name) {
1330
+ return this.dependencies.get(`${encodeURIComponent(type)}:${encodeURIComponent(name)}`) ?? [];
1331
+ }
1332
+ /**
1333
+ * Get metadata items that depend on this item
1334
+ */
1335
+ async getDependents(type, name) {
1336
+ const dependents = [];
1337
+ for (const deps of this.dependencies.values()) {
1338
+ for (const dep of deps) {
1339
+ if (dep.targetType === type && dep.targetName === name) {
1340
+ dependents.push(dep);
1341
+ }
1342
+ }
1343
+ }
1344
+ return dependents;
1345
+ }
1346
+ /**
1347
+ * Register a dependency between two metadata items.
1348
+ * Used internally to track cross-references.
1349
+ * Duplicate dependencies (same source, target, and kind) are ignored.
1350
+ */
1351
+ addDependency(dep) {
1352
+ const key = `${encodeURIComponent(dep.sourceType)}:${encodeURIComponent(dep.sourceName)}`;
1353
+ if (!this.dependencies.has(key)) {
1354
+ this.dependencies.set(key, []);
1355
+ }
1356
+ const existing = this.dependencies.get(key);
1357
+ const isDuplicate = existing.some(
1358
+ (d) => d.targetType === dep.targetType && d.targetName === dep.targetName && d.kind === dep.kind
1359
+ );
1360
+ if (!isDuplicate) {
1361
+ existing.push(dep);
1362
+ }
1363
+ }
1364
+ // ==========================================
1365
+ // Legacy Loader API (backward compatible)
1366
+ // ==========================================
1367
+ /**
1368
+ * Load a single metadata item from loaders.
1369
+ * Iterates through registered loaders until found.
1370
+ */
1371
+ async load(type, name, options) {
1372
+ for (const loader of this.loaders.values()) {
1373
+ try {
1374
+ const result = await loader.load(type, name, options);
1375
+ if (result.data) {
1376
+ return result.data;
1377
+ }
1378
+ } catch (e) {
1379
+ this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
1380
+ }
1381
+ }
1382
+ return null;
1383
+ }
1384
+ /**
1385
+ * Load multiple metadata items from loaders.
1386
+ * Aggregates results from all loaders.
1387
+ */
1388
+ async loadMany(type, options) {
1389
+ const results = [];
1390
+ for (const loader of this.loaders.values()) {
1391
+ try {
1392
+ const items = await loader.loadMany(type, options);
1393
+ for (const item of items) {
1394
+ const itemAny = item;
1395
+ if (itemAny && typeof itemAny.name === "string") {
1396
+ const exists = results.some((r) => r && r.name === itemAny.name);
1397
+ if (exists) continue;
1398
+ }
1399
+ results.push(item);
1400
+ }
1401
+ } catch (e) {
1402
+ this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
1403
+ }
1404
+ }
1405
+ return results;
1406
+ }
1407
+ /**
1408
+ * Save metadata item to a loader
1409
+ */
1410
+ async save(type, name, data, options) {
1411
+ const targetLoader = options?.loader;
1412
+ let loader;
1413
+ if (targetLoader) {
1414
+ loader = this.loaders.get(targetLoader);
1415
+ if (!loader) {
1416
+ throw new Error(`Loader not found: ${targetLoader}`);
1417
+ }
1418
+ } else {
1419
+ for (const l of this.loaders.values()) {
1420
+ if (!l.save) continue;
1421
+ try {
1422
+ if (await l.exists(type, name)) {
1423
+ loader = l;
1424
+ this.logger.info(`Updating existing metadata in loader: ${l.contract.name}`);
1425
+ break;
1426
+ }
1427
+ } catch (e) {
1428
+ }
1429
+ }
1430
+ if (!loader) {
1431
+ const fsLoader = this.loaders.get("filesystem");
1432
+ if (fsLoader && fsLoader.save) {
1433
+ loader = fsLoader;
1434
+ }
1435
+ }
1436
+ if (!loader) {
1437
+ for (const l of this.loaders.values()) {
1438
+ if (l.save) {
1439
+ loader = l;
1440
+ break;
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+ if (!loader) {
1446
+ throw new Error(`No loader available for saving type: ${type}`);
1447
+ }
1448
+ if (!loader.save) {
1449
+ throw new Error(`Loader '${loader.contract?.name}' does not support saving`);
1450
+ }
1451
+ return loader.save(type, name, data, options);
1452
+ }
1453
+ /**
1454
+ * Register a watch callback for metadata changes
1455
+ */
1456
+ addWatchCallback(type, callback) {
1457
+ if (!this.watchCallbacks.has(type)) {
1458
+ this.watchCallbacks.set(type, /* @__PURE__ */ new Set());
1459
+ }
1460
+ this.watchCallbacks.get(type).add(callback);
1461
+ }
1462
+ /**
1463
+ * Remove a watch callback for metadata changes
1464
+ */
1465
+ removeWatchCallback(type, callback) {
1466
+ const callbacks = this.watchCallbacks.get(type);
1467
+ if (callbacks) {
1468
+ callbacks.delete(callback);
1469
+ if (callbacks.size === 0) {
1470
+ this.watchCallbacks.delete(type);
1471
+ }
1472
+ }
1473
+ }
1474
+ /**
1475
+ * Stop all watching
1476
+ */
1477
+ async stopWatching() {
1478
+ }
1479
+ notifyWatchers(type, event) {
1480
+ const callbacks = this.watchCallbacks.get(type);
1481
+ if (!callbacks) return;
1482
+ for (const callback of callbacks) {
1483
+ try {
1484
+ void callback(event);
1485
+ } catch (error) {
1486
+ this.logger.error("Watch callback error", void 0, {
1487
+ type,
1488
+ error: error instanceof Error ? error.message : String(error)
1489
+ });
1490
+ }
1491
+ }
1492
+ }
1493
+ };
1494
+
1495
+ // src/node-metadata-manager.ts
1496
+ var path2 = __toESM(require("path"), 1);
1497
+ var import_chokidar = require("chokidar");
1498
+
1499
+ // src/loaders/filesystem-loader.ts
1500
+ var fs = __toESM(require("fs/promises"), 1);
1501
+ var path = __toESM(require("path"), 1);
1502
+ var import_glob = require("glob");
1503
+ var import_node_crypto = require("crypto");
1504
+ var FilesystemLoader = class {
1505
+ constructor(rootDir, serializers, logger) {
1506
+ this.rootDir = rootDir;
1507
+ this.serializers = serializers;
1508
+ this.logger = logger;
1509
+ this.contract = {
1510
+ name: "filesystem",
1511
+ protocol: "file:",
1512
+ capabilities: {
1513
+ read: true,
1514
+ write: true,
1515
+ watch: true,
1516
+ list: true
1517
+ },
1518
+ supportedFormats: ["json", "yaml", "typescript", "javascript"],
1519
+ supportsWatch: true,
1520
+ supportsWrite: true,
1521
+ supportsCache: true
1522
+ };
1523
+ this.cache = /* @__PURE__ */ new Map();
1524
+ }
1525
+ async load(type, name, options) {
1526
+ const startTime = Date.now();
1527
+ const { validate: _validate = true, useCache = true, ifNoneMatch } = options || {};
1528
+ try {
1529
+ const filePath = await this.findFile(type, name);
1530
+ if (!filePath) {
1531
+ return {
1532
+ data: null,
1533
+ fromCache: false,
1534
+ notModified: false,
1535
+ loadTime: Date.now() - startTime
1536
+ };
1537
+ }
1538
+ const stats = await this.stat(type, name);
1539
+ if (!stats) {
1540
+ return {
1541
+ data: null,
1542
+ fromCache: false,
1543
+ notModified: false,
1544
+ loadTime: Date.now() - startTime
1545
+ };
1546
+ }
1547
+ if (useCache && ifNoneMatch && stats.etag === ifNoneMatch) {
1548
+ return {
1549
+ data: null,
1550
+ fromCache: true,
1551
+ notModified: true,
1552
+ etag: stats.etag,
1553
+ stats,
1554
+ loadTime: Date.now() - startTime
1555
+ };
1556
+ }
1557
+ const cacheKey = `${type}:${name}`;
1558
+ if (useCache && this.cache.has(cacheKey)) {
1559
+ const cached = this.cache.get(cacheKey);
1560
+ if (cached.etag === stats.etag) {
1561
+ return {
1562
+ data: cached.data,
1563
+ fromCache: true,
1564
+ notModified: false,
1565
+ etag: stats.etag,
1566
+ stats,
1567
+ loadTime: Date.now() - startTime
1568
+ };
1569
+ }
1570
+ }
1571
+ const content = await fs.readFile(filePath, "utf-8");
1572
+ const serializer = this.getSerializer(stats.format);
1573
+ if (!serializer) {
1574
+ throw new Error(`No serializer found for format: ${stats.format}`);
1575
+ }
1576
+ const data = serializer.deserialize(content);
1577
+ if (useCache) {
1578
+ this.cache.set(cacheKey, {
1579
+ data,
1580
+ etag: stats.etag || "",
1581
+ timestamp: Date.now()
1582
+ });
1583
+ }
1584
+ return {
1585
+ data,
1586
+ fromCache: false,
1587
+ notModified: false,
1588
+ etag: stats.etag,
1589
+ stats,
1590
+ loadTime: Date.now() - startTime
1591
+ };
1592
+ } catch (error) {
1593
+ this.logger?.error("Failed to load metadata", void 0, {
1594
+ type,
1595
+ name,
1596
+ error: error instanceof Error ? error.message : String(error)
1597
+ });
1598
+ throw error;
1599
+ }
1600
+ }
1601
+ async loadMany(type, options) {
1602
+ const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
1603
+ const typeDir = path.join(this.rootDir, type);
1604
+ const items = [];
1605
+ try {
1606
+ const globPatterns = patterns.map(
1607
+ (pattern) => path.join(typeDir, pattern)
1608
+ );
1609
+ for (const pattern of globPatterns) {
1610
+ const files = await (0, import_glob.glob)(pattern, {
1611
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
1612
+ nodir: true
1613
+ });
1614
+ for (const file of files) {
1615
+ if (limit && items.length >= limit) {
1616
+ break;
1617
+ }
1618
+ try {
1619
+ const content = await fs.readFile(file, "utf-8");
1620
+ const format = this.detectFormat(file);
1621
+ const serializer = this.getSerializer(format);
1622
+ if (serializer) {
1623
+ const data = serializer.deserialize(content);
1624
+ items.push(data);
1625
+ }
1626
+ } catch (error) {
1627
+ this.logger?.warn("Failed to load file", {
1628
+ file,
1629
+ error: error instanceof Error ? error.message : String(error)
1630
+ });
1631
+ }
1632
+ }
1633
+ if (limit && items.length >= limit) {
1634
+ break;
1635
+ }
1636
+ }
1637
+ return items;
1638
+ } catch (error) {
1639
+ this.logger?.error("Failed to load many", void 0, {
1640
+ type,
1641
+ patterns,
1642
+ error: error instanceof Error ? error.message : String(error)
1643
+ });
1644
+ throw error;
1645
+ }
1646
+ }
1647
+ async exists(type, name) {
1648
+ const filePath = await this.findFile(type, name);
1649
+ return filePath !== null;
1650
+ }
1651
+ async stat(type, name) {
1652
+ const filePath = await this.findFile(type, name);
1653
+ if (!filePath) {
1654
+ return null;
1655
+ }
1656
+ try {
1657
+ const stats = await fs.stat(filePath);
1658
+ const content = await fs.readFile(filePath, "utf-8");
1659
+ const etag = this.generateETag(content);
1660
+ const format = this.detectFormat(filePath);
1661
+ return {
1662
+ size: stats.size,
1663
+ modifiedAt: stats.mtime.toISOString(),
1664
+ etag,
1665
+ format,
1666
+ path: filePath
1667
+ };
1668
+ } catch (error) {
1669
+ this.logger?.error("Failed to stat file", void 0, {
1670
+ type,
1671
+ name,
1672
+ filePath,
1673
+ error: error instanceof Error ? error.message : String(error)
1674
+ });
1675
+ return null;
1676
+ }
1677
+ }
1678
+ async list(type) {
1679
+ const typeDir = path.join(this.rootDir, type);
1680
+ try {
1681
+ const files = await (0, import_glob.glob)("**/*", {
1682
+ cwd: typeDir,
1683
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
1684
+ nodir: true
1685
+ });
1686
+ return files.map((file) => {
1687
+ const ext = path.extname(file);
1688
+ const basename3 = path.basename(file, ext);
1689
+ return basename3;
1690
+ });
1691
+ } catch (error) {
1692
+ this.logger?.error("Failed to list", void 0, {
1693
+ type,
1694
+ error: error instanceof Error ? error.message : String(error)
1695
+ });
1696
+ return [];
1697
+ }
1698
+ }
1699
+ async save(type, name, data, options) {
1700
+ const startTime = Date.now();
1701
+ const {
1702
+ format = "typescript",
1703
+ prettify = true,
1704
+ indent = 2,
1705
+ sortKeys = false,
1706
+ backup = false,
1707
+ overwrite = true,
1708
+ atomic = true,
1709
+ path: customPath
1710
+ } = options || {};
1711
+ try {
1712
+ const serializer = this.getSerializer(format);
1713
+ if (!serializer) {
1714
+ throw new Error(`No serializer found for format: ${format}`);
1715
+ }
1716
+ const typeDir = path.join(this.rootDir, type);
1717
+ const fileName = `${name}${serializer.getExtension()}`;
1718
+ const filePath = customPath || path.join(typeDir, fileName);
1719
+ if (!overwrite) {
1720
+ try {
1721
+ await fs.access(filePath);
1722
+ throw new Error(`File already exists: ${filePath}`);
1723
+ } catch (error) {
1724
+ if (error.code !== "ENOENT") {
1725
+ throw error;
1726
+ }
1727
+ }
1728
+ }
1729
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1730
+ let backupPath;
1731
+ if (backup) {
1732
+ try {
1733
+ await fs.access(filePath);
1734
+ backupPath = `${filePath}.bak`;
1735
+ await fs.copyFile(filePath, backupPath);
1736
+ } catch {
1737
+ }
1738
+ }
1739
+ const content = serializer.serialize(data, {
1740
+ prettify,
1741
+ indent,
1742
+ sortKeys
1743
+ });
1744
+ if (atomic) {
1745
+ const tempPath = `${filePath}.tmp`;
1746
+ await fs.writeFile(tempPath, content, "utf-8");
1747
+ await fs.rename(tempPath, filePath);
1748
+ } else {
1749
+ await fs.writeFile(filePath, content, "utf-8");
1750
+ }
1751
+ return {
1752
+ success: true,
1753
+ path: filePath,
1754
+ // format, // Not in schema
1755
+ size: Buffer.byteLength(content, "utf-8"),
1756
+ backupPath,
1757
+ saveTime: Date.now() - startTime
1758
+ };
1759
+ } catch (error) {
1760
+ this.logger?.error("Failed to save metadata", void 0, {
1761
+ type,
1762
+ name,
1763
+ error: error instanceof Error ? error.message : String(error)
1764
+ });
1765
+ throw error;
1766
+ }
1767
+ }
1768
+ /**
1769
+ * Find file for a given type and name
1770
+ */
1771
+ async findFile(type, name) {
1772
+ const typeDir = path.join(this.rootDir, type);
1773
+ const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
1774
+ for (const ext of extensions) {
1775
+ const filePath = path.join(typeDir, `${name}${ext}`);
1776
+ try {
1777
+ await fs.access(filePath);
1778
+ return filePath;
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ return null;
1783
+ }
1784
+ /**
1785
+ * Detect format from file extension
1786
+ */
1787
+ detectFormat(filePath) {
1788
+ const ext = path.extname(filePath).toLowerCase();
1789
+ switch (ext) {
1790
+ case ".json":
1791
+ return "json";
1792
+ case ".yaml":
1793
+ case ".yml":
1794
+ return "yaml";
1795
+ case ".ts":
1796
+ return "typescript";
1797
+ case ".js":
1798
+ return "javascript";
1799
+ default:
1800
+ return "json";
1801
+ }
1802
+ }
1803
+ /**
1804
+ * Get serializer for format
1805
+ */
1806
+ getSerializer(format) {
1807
+ return this.serializers.get(format);
1808
+ }
1809
+ /**
1810
+ * Generate ETag for content
1811
+ * Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
1812
+ * while keeping ETag headers compact (full 64-char hash is overkill for this use case)
1813
+ */
1814
+ generateETag(content) {
1815
+ const hash = (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 32);
1816
+ return `"${hash}"`;
1817
+ }
1818
+ };
1819
+
1820
+ // src/node-metadata-manager.ts
1821
+ var NodeMetadataManager = class extends MetadataManager {
1822
+ constructor(config) {
1823
+ super(config);
1824
+ if (!config.loaders || config.loaders.length === 0) {
1825
+ const rootDir = config.rootDir || process.cwd();
1826
+ this.registerLoader(new FilesystemLoader(rootDir, this.serializers, this.logger));
1827
+ }
1828
+ if (config.watch) {
1829
+ this.startWatching();
1830
+ }
1831
+ }
1832
+ /**
1833
+ * Stop all watching
1834
+ */
1835
+ async stopWatching() {
1836
+ if (this.watcher) {
1837
+ await this.watcher.close();
1838
+ this.watcher = void 0;
1839
+ }
1840
+ }
1841
+ /**
1842
+ * Start watching for file changes
1843
+ */
1844
+ startWatching() {
1845
+ const rootDir = this.config.rootDir || process.cwd();
1846
+ const { ignored = ["**/node_modules/**", "**/*.test.*"], persistent = true } = this.config.watchOptions || {};
1847
+ this.watcher = (0, import_chokidar.watch)(rootDir, {
1848
+ ignored,
1849
+ persistent,
1850
+ ignoreInitial: true
1851
+ });
1852
+ this.watcher.on("add", async (filePath) => {
1853
+ await this.handleFileEvent("added", filePath);
1854
+ });
1855
+ this.watcher.on("change", async (filePath) => {
1856
+ await this.handleFileEvent("changed", filePath);
1857
+ });
1858
+ this.watcher.on("unlink", async (filePath) => {
1859
+ await this.handleFileEvent("deleted", filePath);
1860
+ });
1861
+ this.logger.info("File watcher started", { rootDir });
1862
+ }
1863
+ /**
1864
+ * Handle file change events
1865
+ */
1866
+ async handleFileEvent(eventType, filePath) {
1867
+ const rootDir = this.config.rootDir || process.cwd();
1868
+ const relativePath = path2.relative(rootDir, filePath);
1869
+ const parts = relativePath.split(path2.sep);
1870
+ if (parts.length < 2) {
1871
+ return;
1872
+ }
1873
+ const type = parts[0];
1874
+ const fileName = parts[parts.length - 1];
1875
+ const name = path2.basename(fileName, path2.extname(fileName));
1876
+ let data = void 0;
1877
+ if (eventType !== "deleted") {
1878
+ try {
1879
+ data = await this.load(type, name, { useCache: false });
1880
+ } catch (error) {
1881
+ this.logger.error("Failed to load changed file", void 0, {
1882
+ filePath,
1883
+ error: error instanceof Error ? error.message : String(error)
1884
+ });
1885
+ return;
1886
+ }
1887
+ }
1888
+ const event = {
1889
+ type: eventType,
1890
+ metadataType: type,
1891
+ name,
1892
+ path: filePath,
1893
+ data,
1894
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1895
+ };
1896
+ this.notifyWatchers(type, event);
1897
+ }
1898
+ };
1899
+
1900
+ // src/plugin.ts
1901
+ var import_kernel = require("@objectstack/spec/kernel");
1902
+ var MetadataPlugin = class {
1903
+ constructor(options = {}) {
1904
+ this.name = "com.objectstack.metadata";
1905
+ this.type = "standard";
1906
+ this.version = "1.0.0";
1907
+ this.init = async (ctx) => {
1908
+ ctx.logger.info("Initializing Metadata Manager", {
1909
+ root: this.options.rootDir || process.cwd(),
1910
+ watch: this.options.watch
1911
+ });
1912
+ ctx.registerService("metadata", this.manager);
1913
+ ctx.registerService("app.com.objectstack.metadata", {
1914
+ id: "com.objectstack.metadata",
1915
+ name: "Metadata",
1916
+ version: "1.0.0",
1917
+ type: "plugin",
1918
+ namespace: "sys",
1919
+ objects: [SysMetadataObject]
1920
+ });
1921
+ ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
1922
+ mode: "file-system",
1923
+ features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
1924
+ });
1925
+ };
1926
+ this.start = async (ctx) => {
1927
+ ctx.logger.info("Loading metadata from file system...");
1928
+ const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
1929
+ let totalLoaded = 0;
1930
+ for (const entry of sortedTypes) {
1931
+ try {
1932
+ const items = await this.manager.loadMany(entry.type, {
1933
+ recursive: true
1934
+ });
1935
+ if (items.length > 0) {
1936
+ for (const item of items) {
1937
+ const meta = item;
1938
+ if (meta?.name) {
1939
+ await this.manager.register(entry.type, meta.name, item);
1940
+ }
1941
+ }
1942
+ ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
1943
+ totalLoaded += items.length;
1944
+ }
1945
+ } catch (e) {
1946
+ ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
1947
+ }
1948
+ }
1949
+ ctx.logger.info("Metadata loading complete", {
1950
+ totalItems: totalLoaded,
1951
+ registeredTypes: sortedTypes.length
1952
+ });
1953
+ };
1954
+ this.options = {
1955
+ watch: true,
1956
+ ...options
1957
+ };
1958
+ const rootDir = this.options.rootDir || process.cwd();
1959
+ this.manager = new NodeMetadataManager({
1960
+ rootDir,
1961
+ watch: this.options.watch ?? true,
1962
+ formats: ["yaml", "json", "typescript", "javascript"]
1963
+ });
1964
+ this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
1965
+ }
1966
+ };
1967
+
1968
+ // src/loaders/memory-loader.ts
1969
+ var MemoryLoader = class {
1970
+ constructor() {
1971
+ this.contract = {
1972
+ name: "memory",
1973
+ protocol: "memory:",
1974
+ capabilities: {
1975
+ read: true,
1976
+ write: true,
1977
+ watch: false,
1978
+ list: true
1979
+ }
1980
+ };
1981
+ // Storage: Type -> Name -> Data
1982
+ this.storage = /* @__PURE__ */ new Map();
1983
+ }
1984
+ async load(type, name, _options) {
1985
+ const typeStore = this.storage.get(type);
1986
+ const data = typeStore?.get(name);
1987
+ if (data) {
1988
+ return {
1989
+ data,
1990
+ source: "memory",
1991
+ format: "json",
1992
+ loadTime: 0
1993
+ };
1994
+ }
1995
+ return { data: null };
1996
+ }
1997
+ async loadMany(type, _options) {
1998
+ const typeStore = this.storage.get(type);
1999
+ if (!typeStore) return [];
2000
+ return Array.from(typeStore.values());
2001
+ }
2002
+ async exists(type, name) {
2003
+ return this.storage.get(type)?.has(name) ?? false;
2004
+ }
2005
+ async stat(type, name) {
2006
+ if (await this.exists(type, name)) {
2007
+ return {
2008
+ size: 0,
2009
+ // In-memory
2010
+ mtime: (/* @__PURE__ */ new Date()).toISOString(),
2011
+ format: "json"
2012
+ };
2013
+ }
2014
+ return null;
2015
+ }
2016
+ async list(type) {
2017
+ const typeStore = this.storage.get(type);
2018
+ if (!typeStore) return [];
2019
+ return Array.from(typeStore.keys());
2020
+ }
2021
+ async save(type, name, data, _options) {
2022
+ if (!this.storage.has(type)) {
2023
+ this.storage.set(type, /* @__PURE__ */ new Map());
2024
+ }
2025
+ this.storage.get(type).set(name, data);
2026
+ return {
2027
+ success: true,
2028
+ path: `memory://${type}/${name}`,
2029
+ saveTime: 0
2030
+ };
2031
+ }
2032
+ };
2033
+
2034
+ // src/loaders/remote-loader.ts
2035
+ var RemoteLoader = class {
2036
+ constructor(baseUrl, authToken) {
2037
+ this.baseUrl = baseUrl;
2038
+ this.authToken = authToken;
2039
+ this.contract = {
2040
+ name: "remote",
2041
+ protocol: "http:",
2042
+ capabilities: {
2043
+ read: true,
2044
+ write: true,
2045
+ watch: false,
2046
+ // Could implement SSE/WebSocket in future
2047
+ list: true
2048
+ }
2049
+ };
2050
+ }
2051
+ get headers() {
2052
+ return {
2053
+ "Content-Type": "application/json",
2054
+ ...this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {}
2055
+ };
2056
+ }
2057
+ async load(type, name, _options) {
2058
+ try {
2059
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
2060
+ method: "GET",
2061
+ headers: this.headers
2062
+ });
2063
+ if (response.status === 404) {
2064
+ return { data: null };
2065
+ }
2066
+ if (!response.ok) {
2067
+ throw new Error(`Remote load failed: ${response.statusText}`);
2068
+ }
2069
+ const data = await response.json();
2070
+ return {
2071
+ data,
2072
+ source: this.baseUrl,
2073
+ format: "json",
2074
+ loadTime: 0
2075
+ };
2076
+ } catch (error) {
2077
+ console.error(`RemoteLoader error loading ${type}/${name}`, error);
2078
+ throw error;
2079
+ }
2080
+ }
2081
+ async loadMany(type, _options) {
2082
+ const response = await fetch(`${this.baseUrl}/${type}`, {
2083
+ method: "GET",
2084
+ headers: this.headers
2085
+ });
2086
+ if (!response.ok) {
2087
+ return [];
2088
+ }
2089
+ return await response.json();
2090
+ }
2091
+ async exists(type, name) {
2092
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
2093
+ method: "HEAD",
2094
+ headers: this.headers
2095
+ });
2096
+ return response.ok;
2097
+ }
2098
+ async stat(type, name) {
2099
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
2100
+ method: "HEAD",
2101
+ headers: this.headers
2102
+ });
2103
+ if (!response.ok) return null;
2104
+ return {
2105
+ size: Number(response.headers.get("content-length") || 0),
2106
+ mtime: new Date(response.headers.get("last-modified") || Date.now()).toISOString(),
2107
+ format: "json"
2108
+ };
2109
+ }
2110
+ async list(type) {
2111
+ const items = await this.loadMany(type);
2112
+ return items.map((i) => i.name);
2113
+ }
2114
+ async save(type, name, data, _options) {
2115
+ const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
2116
+ method: "PUT",
2117
+ headers: this.headers,
2118
+ body: JSON.stringify(data)
2119
+ });
2120
+ if (!response.ok) {
2121
+ throw new Error(`Remote save failed: ${response.statusText}`);
2122
+ }
2123
+ return {
2124
+ success: true,
2125
+ path: `${this.baseUrl}/${type}/${name}`,
2126
+ saveTime: 0
2127
+ };
2128
+ }
2129
+ };
2130
+
2131
+ // src/migration/index.ts
2132
+ var migration_exports = {};
2133
+ __export(migration_exports, {
2134
+ MigrationExecutor: () => MigrationExecutor
2135
+ });
2136
+
2137
+ // src/migration/executor.ts
2138
+ var MigrationExecutor = class {
2139
+ constructor(driver) {
2140
+ this.driver = driver;
2141
+ }
2142
+ async executeChangeSet(changeSet) {
2143
+ console.log(`Executing ChangeSet: ${changeSet.name} (${changeSet.id})`);
2144
+ for (const op of changeSet.operations) {
2145
+ try {
2146
+ await this.executeOperation(op);
2147
+ } catch (e) {
2148
+ console.error(`Failed to execute operation ${op.type}:`, e);
2149
+ throw e;
2150
+ }
2151
+ }
2152
+ }
2153
+ async executeOperation(op) {
2154
+ switch (op.type) {
2155
+ case "create_object":
2156
+ console.log(` > Create Object: ${op.object.name}`);
2157
+ await this.driver.createCollection(op.object.name, op.object);
2158
+ break;
2159
+ case "add_field":
2160
+ console.log(` > Add Field: ${op.objectName}.${op.fieldName}`);
2161
+ await this.driver.addColumn(op.objectName, op.fieldName, op.field);
2162
+ break;
2163
+ case "remove_field":
2164
+ console.log(` > Remove Field: ${op.objectName}.${op.fieldName}`);
2165
+ await this.driver.dropColumn(op.objectName, op.fieldName);
2166
+ break;
2167
+ case "delete_object":
2168
+ console.log(` > Delete Object: ${op.objectName}`);
2169
+ await this.driver.dropCollection(op.objectName);
2170
+ break;
2171
+ case "execute_sql":
2172
+ console.log(` > Execute SQL`);
2173
+ await this.driver.executeRaw(op.sql);
2174
+ break;
2175
+ case "modify_field":
2176
+ console.warn(` ! Modify Field: ${op.objectName}.${op.fieldName} (Not fully implemented)`);
2177
+ break;
2178
+ case "rename_object":
2179
+ console.warn(` ! Rename Object: ${op.oldName} -> ${op.newName} (Not fully implemented)`);
2180
+ break;
2181
+ default:
2182
+ throw new Error(`Unknown operation type`);
2183
+ }
2184
+ }
2185
+ };
2186
+ // Annotate the CommonJS export names for ESM import in node:
2187
+ 0 && (module.exports = {
2188
+ DatabaseLoader,
2189
+ FilesystemLoader,
2190
+ JSONSerializer,
2191
+ MemoryLoader,
2192
+ MetadataManager,
2193
+ MetadataPlugin,
2194
+ Migration,
2195
+ NodeMetadataManager,
2196
+ RemoteLoader,
2197
+ SysMetadataObject,
2198
+ TypeScriptSerializer,
2199
+ YAMLSerializer
2200
+ });
2201
+ //# sourceMappingURL=node.cjs.map