@objectstack/metadata 3.2.9 → 3.3.1

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