@objectstack/metadata 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,8 +38,8 @@ __export(index_exports, {
38
38
  MetadataPlugin: () => MetadataPlugin,
39
39
  Migration: () => migration_exports,
40
40
  RemoteLoader: () => RemoteLoader,
41
- SysMetadataHistoryObject: () => SysMetadataHistoryObject,
42
- SysMetadataObject: () => SysMetadataObject,
41
+ SysMetadataHistoryObject: () => import_metadata3.SysMetadataHistoryObject,
42
+ SysMetadataObject: () => import_metadata3.SysMetadataObject,
43
43
  TypeScriptSerializer: () => TypeScriptSerializer,
44
44
  YAMLSerializer: () => YAMLSerializer,
45
45
  calculateChecksum: () => calculateChecksum,
@@ -219,277 +219,8 @@ export default metadata;
219
219
  }
220
220
  };
221
221
 
222
- // src/objects/sys-metadata.object.ts
223
- var import_data = require("@objectstack/spec/data");
224
- var SysMetadataObject = import_data.ObjectSchema.create({
225
- namespace: "sys",
226
- name: "metadata",
227
- label: "System Metadata",
228
- pluralLabel: "System Metadata",
229
- icon: "settings",
230
- isSystem: true,
231
- description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
232
- fields: {
233
- /** Primary Key (UUID) */
234
- id: import_data.Field.text({
235
- label: "ID",
236
- required: true,
237
- readonly: true
238
- }),
239
- /** Machine name — unique identifier used in code references */
240
- name: import_data.Field.text({
241
- label: "Name",
242
- required: true,
243
- searchable: true,
244
- maxLength: 255
245
- }),
246
- /** Metadata type (e.g. "object", "view", "flow") */
247
- type: import_data.Field.text({
248
- label: "Metadata Type",
249
- required: true,
250
- searchable: true,
251
- maxLength: 100
252
- }),
253
- /** Namespace / module grouping (e.g. "crm", "core") */
254
- namespace: import_data.Field.text({
255
- label: "Namespace",
256
- required: false,
257
- defaultValue: "default",
258
- maxLength: 100
259
- }),
260
- /** Package that owns/delivered this metadata */
261
- package_id: import_data.Field.text({
262
- label: "Package ID",
263
- required: false,
264
- maxLength: 255
265
- }),
266
- /** Who manages this record: package, platform, or user */
267
- managed_by: import_data.Field.select(["package", "platform", "user"], {
268
- label: "Managed By",
269
- required: false
270
- }),
271
- /** Scope: system (code), platform (admin DB), user (personal DB) */
272
- scope: import_data.Field.select(["system", "platform", "user"], {
273
- label: "Scope",
274
- required: true,
275
- defaultValue: "platform"
276
- }),
277
- /** JSON payload — the actual metadata configuration */
278
- metadata: import_data.Field.textarea({
279
- label: "Metadata",
280
- required: true,
281
- description: "JSON-serialized metadata payload"
282
- }),
283
- /** Parent metadata name for extension/override */
284
- extends: import_data.Field.text({
285
- label: "Extends",
286
- required: false,
287
- maxLength: 255
288
- }),
289
- /** Merge strategy when extending parent metadata */
290
- strategy: import_data.Field.select(["merge", "replace"], {
291
- label: "Strategy",
292
- required: false,
293
- defaultValue: "merge"
294
- }),
295
- /** Owner user ID (for user-scope items) */
296
- owner: import_data.Field.text({
297
- label: "Owner",
298
- required: false,
299
- maxLength: 255
300
- }),
301
- /** Lifecycle state */
302
- state: import_data.Field.select(["draft", "active", "archived", "deprecated"], {
303
- label: "State",
304
- required: false,
305
- defaultValue: "active"
306
- }),
307
- /** Tenant ID for multi-tenant isolation */
308
- tenant_id: import_data.Field.text({
309
- label: "Tenant ID",
310
- required: false,
311
- maxLength: 255
312
- }),
313
- /** Version number for optimistic concurrency */
314
- version: import_data.Field.number({
315
- label: "Version",
316
- required: false,
317
- defaultValue: 1
318
- }),
319
- /** Content checksum for change detection */
320
- checksum: import_data.Field.text({
321
- label: "Checksum",
322
- required: false,
323
- maxLength: 64
324
- }),
325
- /** Origin of this metadata record */
326
- source: import_data.Field.select(["filesystem", "database", "api", "migration"], {
327
- label: "Source",
328
- required: false
329
- }),
330
- /** Classification tags (JSON array) */
331
- tags: import_data.Field.textarea({
332
- label: "Tags",
333
- required: false,
334
- description: "JSON-serialized array of classification tags"
335
- }),
336
- /** Audit fields */
337
- created_by: import_data.Field.text({
338
- label: "Created By",
339
- required: false,
340
- readonly: true,
341
- maxLength: 255
342
- }),
343
- created_at: import_data.Field.datetime({
344
- label: "Created At",
345
- required: false,
346
- readonly: true
347
- }),
348
- updated_by: import_data.Field.text({
349
- label: "Updated By",
350
- required: false,
351
- maxLength: 255
352
- }),
353
- updated_at: import_data.Field.datetime({
354
- label: "Updated At",
355
- required: false
356
- })
357
- },
358
- indexes: [
359
- { fields: ["type", "name"], unique: true },
360
- { fields: ["type", "scope"] },
361
- { fields: ["tenant_id"] },
362
- { fields: ["state"] },
363
- { fields: ["namespace"] }
364
- ],
365
- enable: {
366
- trackHistory: true,
367
- searchable: false,
368
- apiEnabled: true,
369
- apiMethods: ["get", "list", "create", "update", "delete"],
370
- trash: false
371
- }
372
- });
373
-
374
- // src/objects/sys-metadata-history.object.ts
375
- var import_data2 = require("@objectstack/spec/data");
376
- var SysMetadataHistoryObject = import_data2.ObjectSchema.create({
377
- namespace: "sys",
378
- name: "metadata_history",
379
- label: "Metadata History",
380
- pluralLabel: "Metadata History",
381
- icon: "history",
382
- isSystem: true,
383
- description: "Version history and audit trail for metadata changes",
384
- fields: {
385
- /** Primary Key (UUID) */
386
- id: import_data2.Field.text({
387
- label: "ID",
388
- required: true,
389
- readonly: true
390
- }),
391
- /** Foreign key to sys_metadata.id */
392
- metadata_id: import_data2.Field.text({
393
- label: "Metadata ID",
394
- required: true,
395
- readonly: true,
396
- maxLength: 255
397
- }),
398
- /** Machine name (denormalized for easier querying) */
399
- name: import_data2.Field.text({
400
- label: "Name",
401
- required: true,
402
- searchable: true,
403
- readonly: true,
404
- maxLength: 255
405
- }),
406
- /** Metadata type (denormalized for easier querying) */
407
- type: import_data2.Field.text({
408
- label: "Metadata Type",
409
- required: true,
410
- searchable: true,
411
- readonly: true,
412
- maxLength: 100
413
- }),
414
- /** Version number at this snapshot */
415
- version: import_data2.Field.number({
416
- label: "Version",
417
- required: true,
418
- readonly: true
419
- }),
420
- /** Type of operation that created this history entry */
421
- operation_type: import_data2.Field.select(["create", "update", "publish", "revert", "delete"], {
422
- label: "Operation Type",
423
- required: true,
424
- readonly: true
425
- }),
426
- /** Historical metadata snapshot (JSON payload) */
427
- metadata: import_data2.Field.textarea({
428
- label: "Metadata",
429
- required: true,
430
- readonly: true,
431
- description: "JSON-serialized metadata snapshot at this version"
432
- }),
433
- /** SHA-256 checksum of metadata content */
434
- checksum: import_data2.Field.text({
435
- label: "Checksum",
436
- required: true,
437
- readonly: true,
438
- maxLength: 64
439
- }),
440
- /** Checksum of the previous version */
441
- previous_checksum: import_data2.Field.text({
442
- label: "Previous Checksum",
443
- required: false,
444
- readonly: true,
445
- maxLength: 64
446
- }),
447
- /** Human-readable description of changes */
448
- change_note: import_data2.Field.textarea({
449
- label: "Change Note",
450
- required: false,
451
- readonly: true,
452
- description: "Description of what changed in this version"
453
- }),
454
- /** Tenant ID for multi-tenant isolation */
455
- tenant_id: import_data2.Field.text({
456
- label: "Tenant ID",
457
- required: false,
458
- readonly: true,
459
- maxLength: 255
460
- }),
461
- /** User who made this change */
462
- recorded_by: import_data2.Field.text({
463
- label: "Recorded By",
464
- required: false,
465
- readonly: true,
466
- maxLength: 255
467
- }),
468
- /** When was this version recorded */
469
- recorded_at: import_data2.Field.datetime({
470
- label: "Recorded At",
471
- required: true,
472
- readonly: true
473
- })
474
- },
475
- indexes: [
476
- { fields: ["metadata_id", "version"], unique: true },
477
- { fields: ["metadata_id", "recorded_at"] },
478
- { fields: ["type", "name"] },
479
- { fields: ["recorded_at"] },
480
- { fields: ["operation_type"] },
481
- { fields: ["tenant_id"] }
482
- ],
483
- enable: {
484
- trackHistory: false,
485
- // Don't track history of history records
486
- searchable: false,
487
- apiEnabled: true,
488
- apiMethods: ["get", "list"],
489
- // Read-only via API
490
- trash: false
491
- }
492
- });
222
+ // src/loaders/database-loader.ts
223
+ var import_metadata = require("@objectstack/platform-objects/metadata");
493
224
 
494
225
  // src/utils/metadata-history-utils.ts
495
226
  async function calculateChecksum(metadata) {
@@ -589,6 +320,114 @@ function generateDiffSummary(diff) {
589
320
  return summary.join(", ");
590
321
  }
591
322
 
323
+ // src/utils/lru-cache.ts
324
+ var LRUCache = class {
325
+ constructor(options = {}) {
326
+ this.map = /* @__PURE__ */ new Map();
327
+ this.hits = 0;
328
+ this.misses = 0;
329
+ this.maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : 0;
330
+ this.ttl = options.ttl && options.ttl > 0 ? options.ttl : 0;
331
+ }
332
+ get(key) {
333
+ const entry = this.map.get(key);
334
+ if (!entry) {
335
+ this.misses++;
336
+ return void 0;
337
+ }
338
+ if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
339
+ this.map.delete(key);
340
+ this.misses++;
341
+ return void 0;
342
+ }
343
+ this.map.delete(key);
344
+ this.map.set(key, entry);
345
+ this.hits++;
346
+ return entry.value;
347
+ }
348
+ set(key, value) {
349
+ if (this.map.has(key)) {
350
+ this.map.delete(key);
351
+ } else if (this.maxSize > 0 && this.map.size >= this.maxSize) {
352
+ const oldest = this.map.keys().next();
353
+ if (!oldest.done) this.map.delete(oldest.value);
354
+ }
355
+ this.map.set(key, {
356
+ value,
357
+ expiresAt: this.ttl > 0 ? Date.now() + this.ttl : 0
358
+ });
359
+ }
360
+ has(key) {
361
+ return this.get(key) !== void 0;
362
+ }
363
+ delete(key) {
364
+ return this.map.delete(key);
365
+ }
366
+ clear() {
367
+ this.map.clear();
368
+ }
369
+ get size() {
370
+ return this.map.size;
371
+ }
372
+ /** Diagnostic counters — useful for `metrics` endpoints. */
373
+ stats() {
374
+ const total = this.hits + this.misses;
375
+ return {
376
+ size: this.map.size,
377
+ hits: this.hits,
378
+ misses: this.misses,
379
+ hitRate: total === 0 ? 0 : this.hits / total
380
+ };
381
+ }
382
+ /** Resets hit/miss counters without dropping cached entries. */
383
+ resetStats() {
384
+ this.hits = 0;
385
+ this.misses = 0;
386
+ }
387
+ };
388
+
389
+ // src/migrations/add-sys-metadata-overlay-index.ts
390
+ var INDEX_NAME = "idx_sys_metadata_overlay_active";
391
+ var TABLE = "sys_metadata";
392
+ var COLUMNS = "(type, name, organization_id, project_id, scope)";
393
+ var WHERE = "state = 'active'";
394
+ async function addSysMetadataOverlayIndex(driver) {
395
+ const driverAny = driver;
396
+ const exec = async (sql) => {
397
+ if (typeof driverAny.raw === "function") {
398
+ await driverAny.raw(sql);
399
+ } else if (typeof driverAny.execute === "function") {
400
+ await driverAny.execute(sql);
401
+ } else {
402
+ throw new Error("driver has neither raw nor execute");
403
+ }
404
+ };
405
+ const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
406
+ const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
407
+ try {
408
+ await exec(partialSql);
409
+ return { index: INDEX_NAME, status: "created" };
410
+ } catch (err) {
411
+ const msg = err instanceof Error ? err.message : String(err);
412
+ if (/partial|where clause|syntax/i.test(msg)) {
413
+ try {
414
+ await exec(fallbackSql);
415
+ return { index: INDEX_NAME, status: "fallback_non_unique" };
416
+ } catch (fallbackErr) {
417
+ return {
418
+ index: INDEX_NAME,
419
+ status: "error",
420
+ error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
421
+ };
422
+ }
423
+ }
424
+ if (/already exists/i.test(msg)) {
425
+ return { index: INDEX_NAME, status: "already_exists" };
426
+ }
427
+ return { index: INDEX_NAME, status: "error", error: msg };
428
+ }
429
+ }
430
+
592
431
  // src/loaders/database-loader.ts
593
432
  var DatabaseLoader = class {
594
433
  constructor(options) {
@@ -604,11 +443,103 @@ var DatabaseLoader = class {
604
443
  };
605
444
  this.schemaReady = false;
606
445
  this.historySchemaReady = false;
446
+ if (!options.driver && !options.engine) {
447
+ throw new Error("DatabaseLoader requires either a driver or engine");
448
+ }
607
449
  this.driver = options.driver;
450
+ this.engine = options.engine;
608
451
  this.tableName = options.tableName ?? "sys_metadata";
609
452
  this.historyTableName = options.historyTableName ?? "sys_metadata_history";
610
- this.tenantId = options.tenantId;
453
+ this.organizationId = options.organizationId;
454
+ this.projectId = options.projectId;
611
455
  this.trackHistory = options.trackHistory !== false;
456
+ const cacheOpts = options.cache;
457
+ const cacheEnabled = cacheOpts?.enabled !== false;
458
+ if (cacheEnabled) {
459
+ const lruOpts = {
460
+ maxSize: cacheOpts?.maxSize ?? 500,
461
+ ttl: cacheOpts?.ttl ?? 6e4
462
+ };
463
+ this.loadCache = new LRUCache(lruOpts);
464
+ this.loadManyCache = new LRUCache(lruOpts);
465
+ this.listCache = new LRUCache(lruOpts);
466
+ this.statCache = new LRUCache(lruOpts);
467
+ }
468
+ }
469
+ // ==========================================
470
+ // Cache helpers
471
+ // ==========================================
472
+ cacheKey(type, name) {
473
+ return `${type}::${name}`;
474
+ }
475
+ /**
476
+ * Invalidate all cached entries for a specific (type, name) pair plus
477
+ * the type-level aggregates (`loadMany`, `list`). Called from every write
478
+ * path (`save`, `delete`, `registerRollback`).
479
+ */
480
+ invalidate(type, name) {
481
+ if (!this.loadCache) return;
482
+ const key = this.cacheKey(type, name);
483
+ this.loadCache.delete(key);
484
+ this.statCache?.delete(key);
485
+ this.loadManyCache?.delete(type);
486
+ this.listCache?.delete(type);
487
+ }
488
+ /** Drop the entire cache — useful after bulk imports or schema changes. */
489
+ invalidateAll() {
490
+ this.loadCache?.clear();
491
+ this.loadManyCache?.clear();
492
+ this.listCache?.clear();
493
+ this.statCache?.clear();
494
+ }
495
+ /** Diagnostic: aggregated cache statistics for `metrics` endpoints. */
496
+ getCacheStats() {
497
+ return {
498
+ enabled: this.loadCache !== void 0,
499
+ load: this.loadCache?.stats() ?? null,
500
+ loadMany: this.loadManyCache?.stats() ?? null,
501
+ list: this.listCache?.stats() ?? null,
502
+ stat: this.statCache?.stats() ?? null
503
+ };
504
+ }
505
+ // ==========================================
506
+ // Internal CRUD helpers (driver vs engine)
507
+ // ==========================================
508
+ async _find(table, query) {
509
+ if (this.engine) {
510
+ return this.engine.find(table, query);
511
+ }
512
+ return this.driver.find(table, { object: table, ...query });
513
+ }
514
+ async _findOne(table, query) {
515
+ if (this.engine) {
516
+ return this.engine.findOne(table, query);
517
+ }
518
+ return this.driver.findOne(table, { object: table, ...query });
519
+ }
520
+ async _count(table, query) {
521
+ if (this.engine) {
522
+ return this.engine.count(table, query);
523
+ }
524
+ return this.driver.count(table, { object: table, ...query });
525
+ }
526
+ async _create(table, data) {
527
+ if (this.engine) {
528
+ return this.engine.insert(table, data);
529
+ }
530
+ return this.driver.create(table, data);
531
+ }
532
+ async _update(table, id, data) {
533
+ if (this.engine) {
534
+ return this.engine.update(table, { id, ...data });
535
+ }
536
+ return this.driver.update(table, id, data);
537
+ }
538
+ async _delete(table, id) {
539
+ if (this.engine) {
540
+ return this.engine.delete(table, { where: { id } });
541
+ }
542
+ return this.driver.delete(table, id);
612
543
  }
613
544
  /**
614
545
  * Ensure the metadata table exists.
@@ -617,12 +548,37 @@ var DatabaseLoader = class {
617
548
  */
618
549
  async ensureSchema() {
619
550
  if (this.schemaReady) return;
551
+ if (this.engine) {
552
+ this.schemaReady = true;
553
+ try {
554
+ const engineAny = this.engine;
555
+ let driver = engineAny?.driver ?? engineAny?.getDriver?.();
556
+ if (!driver && engineAny?.drivers instanceof Map) {
557
+ for (const candidate of engineAny.drivers.values()) {
558
+ const c = candidate;
559
+ if (c && (typeof c.raw === "function" || typeof c.execute === "function")) {
560
+ driver = candidate;
561
+ break;
562
+ }
563
+ }
564
+ }
565
+ if (driver) {
566
+ await addSysMetadataOverlayIndex(driver);
567
+ }
568
+ } catch {
569
+ }
570
+ return;
571
+ }
620
572
  try {
621
573
  await this.driver.syncSchema(this.tableName, {
622
- ...SysMetadataObject,
574
+ ...import_metadata.SysMetadataObject,
623
575
  name: this.tableName
624
576
  });
625
577
  this.schemaReady = true;
578
+ try {
579
+ await addSysMetadataOverlayIndex(this.driver);
580
+ } catch {
581
+ }
626
582
  } catch {
627
583
  this.schemaReady = true;
628
584
  }
@@ -633,9 +589,13 @@ var DatabaseLoader = class {
633
589
  */
634
590
  async ensureHistorySchema() {
635
591
  if (!this.trackHistory || this.historySchemaReady) return;
592
+ if (this.engine) {
593
+ this.historySchemaReady = true;
594
+ return;
595
+ }
636
596
  try {
637
597
  await this.driver.syncSchema(this.historyTableName, {
638
- ...SysMetadataHistoryObject,
598
+ ...import_metadata.SysMetadataHistoryObject,
639
599
  name: this.historyTableName
640
600
  });
641
601
  this.historySchemaReady = true;
@@ -645,16 +605,18 @@ var DatabaseLoader = class {
645
605
  }
646
606
  /**
647
607
  * Build base filter conditions for queries.
648
- * Always includes tenantId when configured.
608
+ * Filters by organizationId when configured; project_id when projectId is set,
609
+ * or null (platform-global) when not set.
649
610
  */
650
611
  baseFilter(type, name) {
651
612
  const filter = { type };
652
613
  if (name !== void 0) {
653
614
  filter.name = name;
654
615
  }
655
- if (this.tenantId) {
656
- filter.tenant_id = this.tenantId;
616
+ if (this.organizationId) {
617
+ filter.organization_id = this.organizationId;
657
618
  }
619
+ filter.project_id = this.projectId ?? null;
658
620
  return filter;
659
621
  }
660
622
  /**
@@ -693,10 +655,11 @@ var DatabaseLoader = class {
693
655
  changeNote,
694
656
  recordedBy,
695
657
  recordedAt: now,
696
- ...this.tenantId ? { tenantId: this.tenantId } : {}
658
+ ...this.organizationId ? { organizationId: this.organizationId } : {},
659
+ ...this.projectId !== void 0 ? { projectId: this.projectId } : {}
697
660
  };
698
661
  try {
699
- await this.driver.create(this.historyTableName, {
662
+ await this._create(this.historyTableName, {
700
663
  id: historyRecord.id,
701
664
  metadata_id: historyRecord.metadataId,
702
665
  name: historyRecord.name,
@@ -709,7 +672,8 @@ var DatabaseLoader = class {
709
672
  change_note: historyRecord.changeNote,
710
673
  recorded_by: historyRecord.recordedBy,
711
674
  recorded_at: historyRecord.recordedAt,
712
- ...this.tenantId ? { tenant_id: this.tenantId } : {}
675
+ ...this.organizationId ? { organization_id: this.organizationId } : {},
676
+ ...this.projectId !== void 0 ? { project_id: this.projectId } : {}
713
677
  });
714
678
  } catch (error) {
715
679
  console.error(`Failed to create history record for ${type}/${name}:`, error);
@@ -741,7 +705,8 @@ var DatabaseLoader = class {
741
705
  strategy: row.strategy ?? "merge",
742
706
  owner: row.owner,
743
707
  state: row.state ?? "active",
744
- tenantId: row.tenant_id,
708
+ organizationId: row.organization_id,
709
+ projectId: row.project_id,
745
710
  version: row.version ?? 1,
746
711
  checksum: row.checksum,
747
712
  source: row.source,
@@ -758,12 +723,24 @@ var DatabaseLoader = class {
758
723
  async load(type, name, _options) {
759
724
  const startTime = Date.now();
760
725
  await this.ensureSchema();
726
+ const key = this.cacheKey(type, name);
727
+ if (this.loadCache) {
728
+ const cached = this.loadCache.get(key);
729
+ if (cached !== void 0) {
730
+ return {
731
+ data: cached,
732
+ source: "database",
733
+ format: "json",
734
+ loadTime: Date.now() - startTime
735
+ };
736
+ }
737
+ }
761
738
  try {
762
- const row = await this.driver.findOne(this.tableName, {
763
- object: this.tableName,
739
+ const row = await this._findOne(this.tableName, {
764
740
  where: this.baseFilter(type, name)
765
741
  });
766
742
  if (!row) {
743
+ this.loadCache?.set(key, null);
767
744
  return {
768
745
  data: null,
769
746
  loadTime: Date.now() - startTime
@@ -771,6 +748,7 @@ var DatabaseLoader = class {
771
748
  }
772
749
  const data = this.rowToData(row);
773
750
  const record = this.rowToRecord(row);
751
+ this.loadCache?.set(key, data);
774
752
  return {
775
753
  data,
776
754
  source: "database",
@@ -787,21 +765,29 @@ var DatabaseLoader = class {
787
765
  }
788
766
  async loadMany(type, _options) {
789
767
  await this.ensureSchema();
768
+ if (this.loadManyCache) {
769
+ const cached = this.loadManyCache.get(type);
770
+ if (cached !== void 0) return cached;
771
+ }
790
772
  try {
791
- const rows = await this.driver.find(this.tableName, {
792
- object: this.tableName,
773
+ const rows = await this._find(this.tableName, {
793
774
  where: this.baseFilter(type)
794
775
  });
795
- return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
776
+ const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
777
+ this.loadManyCache?.set(type, result);
778
+ return result;
796
779
  } catch {
797
780
  return [];
798
781
  }
799
782
  }
800
783
  async exists(type, name) {
801
784
  await this.ensureSchema();
785
+ if (this.loadCache) {
786
+ const cached = this.loadCache.get(this.cacheKey(type, name));
787
+ if (cached !== void 0) return cached !== null;
788
+ }
802
789
  try {
803
- const count = await this.driver.count(this.tableName, {
804
- object: this.tableName,
790
+ const count = await this._count(this.tableName, {
805
791
  where: this.baseFilter(type, name)
806
792
  });
807
793
  return count > 0;
@@ -811,33 +797,47 @@ var DatabaseLoader = class {
811
797
  }
812
798
  async stat(type, name) {
813
799
  await this.ensureSchema();
800
+ const key = this.cacheKey(type, name);
801
+ if (this.statCache) {
802
+ const cached = this.statCache.get(key);
803
+ if (cached !== void 0) return cached;
804
+ }
814
805
  try {
815
- const row = await this.driver.findOne(this.tableName, {
816
- object: this.tableName,
806
+ const row = await this._findOne(this.tableName, {
817
807
  where: this.baseFilter(type, name)
818
808
  });
819
- if (!row) return null;
809
+ if (!row) {
810
+ this.statCache?.set(key, null);
811
+ return null;
812
+ }
820
813
  const record = this.rowToRecord(row);
821
814
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
822
- return {
815
+ const stats = {
823
816
  size: metadataStr.length,
824
817
  mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
825
818
  format: "json",
826
819
  etag: record.checksum
827
820
  };
821
+ this.statCache?.set(key, stats);
822
+ return stats;
828
823
  } catch {
829
824
  return null;
830
825
  }
831
826
  }
832
827
  async list(type) {
833
828
  await this.ensureSchema();
829
+ if (this.listCache) {
830
+ const cached = this.listCache.get(type);
831
+ if (cached !== void 0) return cached;
832
+ }
834
833
  try {
835
- const rows = await this.driver.find(this.tableName, {
836
- object: this.tableName,
834
+ const rows = await this._find(this.tableName, {
837
835
  where: this.baseFilter(type),
838
836
  fields: ["name"]
839
837
  });
840
- return rows.map((row) => row.name).filter((name) => typeof name === "string");
838
+ const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
839
+ this.listCache?.set(type, names);
840
+ return names;
841
841
  } catch {
842
842
  return [];
843
843
  }
@@ -849,8 +849,7 @@ var DatabaseLoader = class {
849
849
  async getHistoryRecord(type, name, version) {
850
850
  if (!this.trackHistory) return null;
851
851
  await this.ensureHistorySchema();
852
- const metadataRow = await this.driver.findOne(this.tableName, {
853
- object: this.tableName,
852
+ const metadataRow = await this._findOne(this.tableName, {
854
853
  where: this.baseFilter(type, name)
855
854
  });
856
855
  if (!metadataRow) return null;
@@ -858,11 +857,11 @@ var DatabaseLoader = class {
858
857
  metadata_id: metadataRow.id,
859
858
  version
860
859
  };
861
- if (this.tenantId) {
862
- filter.tenant_id = this.tenantId;
860
+ if (this.organizationId) {
861
+ filter.organization_id = this.organizationId;
863
862
  }
864
- const row = await this.driver.findOne(this.historyTableName, {
865
- object: this.historyTableName,
863
+ filter.project_id = this.projectId ?? null;
864
+ const row = await this._findOne(this.historyTableName, {
866
865
  where: filter
867
866
  });
868
867
  if (!row) return null;
@@ -877,11 +876,80 @@ var DatabaseLoader = class {
877
876
  checksum: row.checksum,
878
877
  previousChecksum: row.previous_checksum,
879
878
  changeNote: row.change_note,
880
- tenantId: row.tenant_id,
879
+ organizationId: row.organization_id,
880
+ projectId: row.project_id,
881
881
  recordedBy: row.recorded_by,
882
882
  recordedAt: row.recorded_at
883
883
  };
884
884
  }
885
+ /**
886
+ * Query history records with pagination and filtering.
887
+ * Encapsulates history table queries so MetadataManager doesn't need
888
+ * direct driver access.
889
+ */
890
+ async queryHistory(type, name, options) {
891
+ if (!this.trackHistory) {
892
+ return { records: [], total: 0, hasMore: false };
893
+ }
894
+ await this.ensureSchema();
895
+ await this.ensureHistorySchema();
896
+ const filter = { type, name };
897
+ if (this.organizationId) filter.organization_id = this.organizationId;
898
+ filter.project_id = this.projectId ?? null;
899
+ const metadataRecord = await this._findOne(this.tableName, { where: filter });
900
+ if (!metadataRecord) {
901
+ return { records: [], total: 0, hasMore: false };
902
+ }
903
+ const historyFilter = {
904
+ metadata_id: metadataRecord.id
905
+ };
906
+ if (this.organizationId) historyFilter.organization_id = this.organizationId;
907
+ historyFilter.project_id = this.projectId ?? null;
908
+ if (options?.operationType) historyFilter.operation_type = options.operationType;
909
+ if (options?.since) historyFilter.recorded_at = { $gte: options.since };
910
+ if (options?.until) {
911
+ if (historyFilter.recorded_at) {
912
+ historyFilter.recorded_at.$lte = options.until;
913
+ } else {
914
+ historyFilter.recorded_at = { $lte: options.until };
915
+ }
916
+ }
917
+ const limit = options?.limit ?? 50;
918
+ const offset = options?.offset ?? 0;
919
+ const historyRecords = await this._find(this.historyTableName, {
920
+ where: historyFilter,
921
+ orderBy: [
922
+ { field: "recorded_at", order: "desc" },
923
+ { field: "version", order: "desc" }
924
+ ],
925
+ limit: limit + 1,
926
+ offset
927
+ });
928
+ const hasMore = historyRecords.length > limit;
929
+ const records = historyRecords.slice(0, limit);
930
+ const total = await this._count(this.historyTableName, { where: historyFilter });
931
+ const includeMetadata = options?.includeMetadata !== false;
932
+ const result = records.map((row) => {
933
+ const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
934
+ return {
935
+ id: row.id,
936
+ metadataId: row.metadata_id,
937
+ name: row.name,
938
+ type: row.type,
939
+ version: row.version,
940
+ operationType: row.operation_type,
941
+ metadata: includeMetadata ? parsedMetadata : null,
942
+ checksum: row.checksum,
943
+ previousChecksum: row.previous_checksum,
944
+ changeNote: row.change_note,
945
+ organizationId: row.organization_id,
946
+ projectId: row.project_id,
947
+ recordedBy: row.recorded_by,
948
+ recordedAt: row.recorded_at
949
+ };
950
+ });
951
+ return { records: result, total, hasMore };
952
+ }
885
953
  /**
886
954
  * Perform a rollback: persist `restoredData` as the new current state and record a
887
955
  * single 'revert' history entry (instead of the usual 'update' entry that `save()`
@@ -894,8 +962,7 @@ var DatabaseLoader = class {
894
962
  const now = (/* @__PURE__ */ new Date()).toISOString();
895
963
  const metadataJson = JSON.stringify(restoredData);
896
964
  const newChecksum = await calculateChecksum(restoredData);
897
- const existing = await this.driver.findOne(this.tableName, {
898
- object: this.tableName,
965
+ const existing = await this._findOne(this.tableName, {
899
966
  where: this.baseFilter(type, name)
900
967
  });
901
968
  if (!existing) {
@@ -903,13 +970,14 @@ var DatabaseLoader = class {
903
970
  }
904
971
  const previousChecksum = existing.checksum;
905
972
  const newVersion = (existing.version ?? 0) + 1;
906
- await this.driver.update(this.tableName, existing.id, {
973
+ await this._update(this.tableName, existing.id, {
907
974
  metadata: metadataJson,
908
975
  version: newVersion,
909
976
  checksum: newChecksum,
910
977
  updated_at: now,
911
978
  state: "active"
912
979
  });
980
+ this.invalidate(type, name);
913
981
  await this.createHistoryRecord(
914
982
  existing.id,
915
983
  type,
@@ -929,13 +997,13 @@ var DatabaseLoader = class {
929
997
  const metadataJson = JSON.stringify(data);
930
998
  const newChecksum = await calculateChecksum(data);
931
999
  try {
932
- const existing = await this.driver.findOne(this.tableName, {
933
- object: this.tableName,
1000
+ const existing = await this._findOne(this.tableName, {
934
1001
  where: this.baseFilter(type, name)
935
1002
  });
936
1003
  if (existing) {
937
1004
  const previousChecksum = existing.checksum;
938
1005
  if (newChecksum === previousChecksum) {
1006
+ this.loadCache?.set(this.cacheKey(type, name), data);
939
1007
  return {
940
1008
  success: true,
941
1009
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -944,13 +1012,14 @@ var DatabaseLoader = class {
944
1012
  };
945
1013
  }
946
1014
  const version = (existing.version ?? 0) + 1;
947
- await this.driver.update(this.tableName, existing.id, {
1015
+ await this._update(this.tableName, existing.id, {
948
1016
  metadata: metadataJson,
949
1017
  version,
950
1018
  checksum: newChecksum,
951
1019
  updated_at: now,
952
1020
  state: "active"
953
1021
  });
1022
+ this.invalidate(type, name);
954
1023
  await this.createHistoryRecord(
955
1024
  existing.id,
956
1025
  type,
@@ -968,7 +1037,7 @@ var DatabaseLoader = class {
968
1037
  };
969
1038
  } else {
970
1039
  const id = generateId();
971
- await this.driver.create(this.tableName, {
1040
+ await this._create(this.tableName, {
972
1041
  id,
973
1042
  name,
974
1043
  type,
@@ -980,10 +1049,12 @@ var DatabaseLoader = class {
980
1049
  state: "active",
981
1050
  version: 1,
982
1051
  source: "database",
983
- ...this.tenantId ? { tenant_id: this.tenantId } : {},
1052
+ ...this.organizationId ? { organization_id: this.organizationId } : {},
1053
+ ...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
984
1054
  created_at: now,
985
1055
  updated_at: now
986
1056
  });
1057
+ this.invalidate(type, name);
987
1058
  await this.createHistoryRecord(
988
1059
  id,
989
1060
  type,
@@ -1010,14 +1081,14 @@ var DatabaseLoader = class {
1010
1081
  */
1011
1082
  async delete(type, name) {
1012
1083
  await this.ensureSchema();
1013
- const existing = await this.driver.findOne(this.tableName, {
1014
- object: this.tableName,
1084
+ const existing = await this._findOne(this.tableName, {
1015
1085
  where: this.baseFilter(type, name)
1016
1086
  });
1017
1087
  if (!existing) {
1018
1088
  return;
1019
1089
  }
1020
- await this.driver.delete(this.tableName, existing.id);
1090
+ await this._delete(this.tableName, existing.id);
1091
+ this.invalidate(type, name);
1021
1092
  }
1022
1093
  };
1023
1094
  function generateId() {
@@ -1028,7 +1099,7 @@ function generateId() {
1028
1099
  }
1029
1100
 
1030
1101
  // src/metadata-manager.ts
1031
- var MetadataManager = class {
1102
+ var _MetadataManager = class _MetadataManager {
1032
1103
  constructor(config) {
1033
1104
  this.loaders = /* @__PURE__ */ new Map();
1034
1105
  this.watchCallbacks = /* @__PURE__ */ new Map();
@@ -1040,6 +1111,18 @@ var MetadataManager = class {
1040
1111
  this.typeRegistry = [];
1041
1112
  // Dependency tracking: "type:name" -> dependencies
1042
1113
  this.dependencies = /* @__PURE__ */ new Map();
1114
+ // Short-lived cache for list() results. Built primarily to break the
1115
+ // deadlock that occurs when security/permission middleware calls
1116
+ // `list('permission')` from inside a user-initiated DB transaction: the
1117
+ // DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
1118
+ // acquire a fresh knex connection while the transaction is still holding
1119
+ // SQLite's single connection — knex waits the full `acquireConnectionTimeout`
1120
+ // (60s) before returning []. The cache absorbs the repeated lookups so the
1121
+ // loader is only hit once per TTL window.
1122
+ //
1123
+ // Invalidated on every `register()` / `unregister()` to keep CRUD writes
1124
+ // visible to subsequent reads.
1125
+ this.listCache = /* @__PURE__ */ new Map();
1043
1126
  this.config = config;
1044
1127
  this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
1045
1128
  this.serializers = /* @__PURE__ */ new Map();
@@ -1074,16 +1157,57 @@ var MetadataManager = class {
1074
1157
  * Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
1075
1158
  *
1076
1159
  * @param driver - An IDataDriver instance for database operations
1077
- */
1078
- setDatabaseDriver(driver) {
1160
+ * @param organizationId - Organization ID for multi-tenant isolation
1161
+ * @param projectId - Project ID (undefined = platform-global)
1162
+ */
1163
+ setDatabaseDriver(driver, organizationId, projectId) {
1164
+ if (projectId !== void 0) {
1165
+ this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1166
+ organizationId,
1167
+ projectId
1168
+ });
1169
+ return;
1170
+ }
1079
1171
  const tableName = this.config.tableName ?? "sys_metadata";
1080
1172
  const dbLoader = new DatabaseLoader({
1081
1173
  driver,
1082
- tableName
1174
+ tableName,
1175
+ organizationId,
1176
+ projectId,
1177
+ cache: this.config.cache?.databaseLoader
1083
1178
  });
1084
1179
  this.registerLoader(dbLoader);
1085
1180
  this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
1086
1181
  }
1182
+ /**
1183
+ * Configure and register a DatabaseLoader backed by an IDataEngine (ObjectQL).
1184
+ * The engine handles datasource routing automatically — sys_metadata will
1185
+ * be routed to the correct driver via the standard namespace mapping.
1186
+ * No manual driver resolution needed.
1187
+ *
1188
+ * @param engine - An IDataEngine instance (typically the ObjectQL service)
1189
+ * @param organizationId - Organization ID for multi-tenant isolation
1190
+ * @param projectId - Project ID (undefined = platform-global)
1191
+ */
1192
+ setDataEngine(engine, organizationId, projectId) {
1193
+ if (projectId !== void 0) {
1194
+ this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1195
+ organizationId,
1196
+ projectId
1197
+ });
1198
+ return;
1199
+ }
1200
+ const tableName = this.config.tableName ?? "sys_metadata";
1201
+ const dbLoader = new DatabaseLoader({
1202
+ engine,
1203
+ tableName,
1204
+ organizationId,
1205
+ projectId,
1206
+ cache: this.config.cache?.databaseLoader
1207
+ });
1208
+ this.registerLoader(dbLoader);
1209
+ this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
1210
+ }
1087
1211
  /**
1088
1212
  * Set the realtime service for publishing metadata change events.
1089
1213
  * Should be called after kernel resolves the realtime service.
@@ -1111,10 +1235,19 @@ var MetadataManager = class {
1111
1235
  * should not be written to during runtime registration.
1112
1236
  */
1113
1237
  async register(type, name, data) {
1238
+ if (this.config.persistence?.writable === false) {
1239
+ const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
1240
+ if (this.config.validation?.throwOnError) {
1241
+ throw new Error(msg);
1242
+ }
1243
+ this.logger.warn(msg);
1244
+ return;
1245
+ }
1114
1246
  if (!this.registry.has(type)) {
1115
1247
  this.registry.set(type, /* @__PURE__ */ new Map());
1116
1248
  }
1117
1249
  this.registry.get(type).set(name, data);
1250
+ this.invalidateListCache(type);
1118
1251
  for (const loader of this.loaders.values()) {
1119
1252
  if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
1120
1253
  await loader.save(type, name, data);
@@ -1156,6 +1289,10 @@ var MetadataManager = class {
1156
1289
  * List all metadata items of a given type
1157
1290
  */
1158
1291
  async list(type) {
1292
+ const cached = this.listCache.get(type);
1293
+ if (cached && Date.now() - cached.ts < _MetadataManager.LIST_CACHE_TTL_MS) {
1294
+ return cached.items;
1295
+ }
1159
1296
  const items = /* @__PURE__ */ new Map();
1160
1297
  const typeStore = this.registry.get(type);
1161
1298
  if (typeStore) {
@@ -1176,7 +1313,16 @@ var MetadataManager = class {
1176
1313
  this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
1177
1314
  }
1178
1315
  }
1179
- return Array.from(items.values());
1316
+ const result = Array.from(items.values());
1317
+ this.cacheListResult(type, result);
1318
+ return result;
1319
+ }
1320
+ cacheListResult(type, items) {
1321
+ this.listCache.set(type, { ts: Date.now(), items });
1322
+ }
1323
+ /** Internal helper: drop the cached `list()` result for a type. */
1324
+ invalidateListCache(type) {
1325
+ this.listCache.delete(type);
1180
1326
  }
1181
1327
  /**
1182
1328
  * Unregister/remove a metadata item by type and name.
@@ -1190,6 +1336,7 @@ var MetadataManager = class {
1190
1336
  this.registry.delete(type);
1191
1337
  }
1192
1338
  }
1339
+ this.invalidateListCache(type);
1193
1340
  for (const loader of this.loaders.values()) {
1194
1341
  if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
1195
1342
  if (typeof loader.delete === "function") {
@@ -1604,6 +1751,14 @@ var MetadataManager = class {
1604
1751
  * Save/update an overlay for a metadata item
1605
1752
  */
1606
1753
  async saveOverlay(overlay) {
1754
+ if (this.config.persistence?.overlayWritable === false) {
1755
+ const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
1756
+ if (this.config.validation?.throwOnError) {
1757
+ throw new Error(msg);
1758
+ }
1759
+ this.logger.warn(msg);
1760
+ return;
1761
+ }
1607
1762
  const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
1608
1763
  this.overlays.set(key, overlay);
1609
1764
  }
@@ -1997,84 +2152,14 @@ var MetadataManager = class {
1997
2152
  if (!dbLoader) {
1998
2153
  throw new Error("History tracking requires a database loader to be configured");
1999
2154
  }
2000
- const driver = dbLoader.driver;
2001
- const tableName = dbLoader.tableName;
2002
- const historyTableName = dbLoader.historyTableName;
2003
- const tenantId = dbLoader.tenantId;
2004
- const filter = { type, name };
2005
- if (tenantId) {
2006
- filter.tenant_id = tenantId;
2007
- }
2008
- const metadataRecord = await driver.findOne(tableName, {
2009
- object: tableName,
2010
- where: filter
2011
- });
2012
- if (!metadataRecord) {
2013
- return {
2014
- records: [],
2015
- total: 0,
2016
- hasMore: false
2017
- };
2018
- }
2019
- const historyFilter = {
2020
- metadata_id: metadataRecord.id
2021
- };
2022
- if (tenantId) {
2023
- historyFilter.tenant_id = tenantId;
2024
- }
2025
- if (options?.operationType) {
2026
- historyFilter.operation_type = options.operationType;
2027
- }
2028
- if (options?.since) {
2029
- historyFilter.recorded_at = { $gte: options.since };
2030
- }
2031
- if (options?.until) {
2032
- if (historyFilter.recorded_at) {
2033
- historyFilter.recorded_at.$lte = options.until;
2034
- } else {
2035
- historyFilter.recorded_at = { $lte: options.until };
2036
- }
2037
- }
2038
- const limit = options?.limit ?? 50;
2039
- const offset = options?.offset ?? 0;
2040
- const historyRecords = await driver.find(historyTableName, {
2041
- object: historyTableName,
2042
- where: historyFilter,
2043
- orderBy: [{ field: "recorded_at", order: "desc" }],
2044
- limit: limit + 1,
2045
- // Fetch one extra to determine hasMore
2046
- offset
2155
+ return dbLoader.queryHistory(type, name, {
2156
+ operationType: options?.operationType,
2157
+ since: options?.since,
2158
+ until: options?.until,
2159
+ limit: options?.limit,
2160
+ offset: options?.offset,
2161
+ includeMetadata: options?.includeMetadata
2047
2162
  });
2048
- const hasMore = historyRecords.length > limit;
2049
- const records = historyRecords.slice(0, limit);
2050
- const total = await driver.count(historyTableName, {
2051
- object: historyTableName,
2052
- where: historyFilter
2053
- });
2054
- const includeMetadata = options?.includeMetadata !== false;
2055
- const historyResult = records.map((row) => {
2056
- const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
2057
- return {
2058
- id: row.id,
2059
- metadataId: row.metadata_id,
2060
- name: row.name,
2061
- type: row.type,
2062
- version: row.version,
2063
- operationType: row.operation_type,
2064
- metadata: includeMetadata ? parsedMetadata : null,
2065
- checksum: row.checksum,
2066
- previousChecksum: row.previous_checksum,
2067
- changeNote: row.change_note,
2068
- tenantId: row.tenant_id,
2069
- recordedBy: row.recorded_by,
2070
- recordedAt: row.recorded_at
2071
- };
2072
- });
2073
- return {
2074
- records: historyResult,
2075
- total,
2076
- hasMore
2077
- };
2078
2163
  }
2079
2164
  /**
2080
2165
  * Rollback a metadata item to a specific version.
@@ -2143,6 +2228,12 @@ var MetadataManager = class {
2143
2228
  };
2144
2229
  }
2145
2230
  };
2231
+ _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2232
+ var MetadataManager = _MetadataManager;
2233
+
2234
+ // src/plugin.ts
2235
+ var import_promises = require("fs/promises");
2236
+ var import_node_crypto2 = require("crypto");
2146
2237
 
2147
2238
  // src/node-metadata-manager.ts
2148
2239
  var path2 = __toESM(require("path"), 1);
@@ -2260,7 +2351,7 @@ var FilesystemLoader = class {
2260
2351
  );
2261
2352
  for (const pattern of globPatterns) {
2262
2353
  const files = await (0, import_glob.glob)(pattern, {
2263
- ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
2354
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
2264
2355
  nodir: true
2265
2356
  });
2266
2357
  for (const file of files) {
@@ -2549,124 +2640,6 @@ var NodeMetadataManager = class extends MetadataManager {
2549
2640
  }
2550
2641
  };
2551
2642
 
2552
- // src/plugin.ts
2553
- var import_kernel = require("@objectstack/spec/kernel");
2554
- var MetadataPlugin = class {
2555
- constructor(options = {}) {
2556
- this.name = "com.objectstack.metadata";
2557
- this.type = "standard";
2558
- this.version = "1.0.0";
2559
- this.init = async (ctx) => {
2560
- ctx.logger.info("Initializing Metadata Manager", {
2561
- root: this.options.rootDir || process.cwd(),
2562
- watch: this.options.watch
2563
- });
2564
- ctx.registerService("metadata", this.manager);
2565
- console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
2566
- try {
2567
- ctx.getService("manifest").register({
2568
- id: "com.objectstack.metadata",
2569
- name: "Metadata",
2570
- version: "1.0.0",
2571
- type: "plugin",
2572
- namespace: "sys",
2573
- objects: [SysMetadataObject]
2574
- });
2575
- } catch {
2576
- }
2577
- ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
2578
- mode: "file-system",
2579
- features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
2580
- });
2581
- };
2582
- this.start = async (ctx) => {
2583
- ctx.logger.info("Loading metadata from file system...");
2584
- const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
2585
- let totalLoaded = 0;
2586
- for (const entry of sortedTypes) {
2587
- try {
2588
- const items = await this.manager.loadMany(entry.type, {
2589
- recursive: true
2590
- });
2591
- if (items.length > 0) {
2592
- for (const item of items) {
2593
- const meta = item;
2594
- if (meta?.name) {
2595
- await this.manager.register(entry.type, meta.name, item);
2596
- }
2597
- }
2598
- ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
2599
- totalLoaded += items.length;
2600
- }
2601
- } catch (e) {
2602
- ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
2603
- }
2604
- }
2605
- ctx.logger.info("Metadata loading complete", {
2606
- totalItems: totalLoaded,
2607
- registeredTypes: sortedTypes.length
2608
- });
2609
- let driverBridged = false;
2610
- try {
2611
- const ql = ctx.getService("objectql");
2612
- if (ql) {
2613
- const tableName = this.manager["config"]?.tableName ?? "sys_metadata";
2614
- const driver = ql.getDriverForObject?.(tableName);
2615
- if (driver) {
2616
- ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager via ObjectQL routing", {
2617
- tableName,
2618
- driver: driver.name
2619
- });
2620
- this.manager.setDatabaseDriver(driver);
2621
- driverBridged = true;
2622
- } else {
2623
- ctx.logger.debug("[MetadataPlugin] ObjectQL could not resolve driver for metadata table", { tableName });
2624
- }
2625
- }
2626
- } catch {
2627
- }
2628
- if (!driverBridged) {
2629
- try {
2630
- const services = ctx.getServices();
2631
- for (const [serviceName, service] of services) {
2632
- if (serviceName.startsWith("driver.") && service) {
2633
- ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager (fallback: first driver)", {
2634
- driverService: serviceName
2635
- });
2636
- this.manager.setDatabaseDriver(service);
2637
- break;
2638
- }
2639
- }
2640
- } catch (e) {
2641
- ctx.logger.debug("[MetadataPlugin] No driver service found", { error: e.message });
2642
- }
2643
- }
2644
- try {
2645
- const realtimeService = ctx.getService("realtime");
2646
- if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
2647
- ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
2648
- this.manager.setRealtimeService(realtimeService);
2649
- }
2650
- } catch (e) {
2651
- ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
2652
- error: e.message
2653
- });
2654
- }
2655
- };
2656
- this.options = {
2657
- watch: true,
2658
- ...options
2659
- };
2660
- const rootDir = this.options.rootDir || process.cwd();
2661
- this.manager = new NodeMetadataManager({
2662
- rootDir,
2663
- watch: this.options.watch ?? true,
2664
- formats: ["yaml", "json", "typescript", "javascript"]
2665
- });
2666
- this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
2667
- }
2668
- };
2669
-
2670
2643
  // src/loaders/memory-loader.ts
2671
2644
  var MemoryLoader = class {
2672
2645
  constructor() {
@@ -2745,6 +2718,286 @@ var MemoryLoader = class {
2745
2718
  }
2746
2719
  };
2747
2720
 
2721
+ // src/plugin.ts
2722
+ var import_kernel = require("@objectstack/spec/kernel");
2723
+ var import_metadata2 = require("@objectstack/platform-objects/metadata");
2724
+ var queryableMetadataObjects = [
2725
+ import_metadata2.SysMetadataObject,
2726
+ import_metadata2.SysMetadataHistoryObject
2727
+ ];
2728
+ var ARTIFACT_FIELD_TO_TYPE = {
2729
+ objects: "object",
2730
+ objectExtensions: "object_extension",
2731
+ apps: "app",
2732
+ views: "view",
2733
+ pages: "page",
2734
+ dashboards: "dashboard",
2735
+ reports: "report",
2736
+ actions: "action",
2737
+ themes: "theme",
2738
+ workflows: "workflow",
2739
+ approvals: "approval",
2740
+ flows: "flow",
2741
+ roles: "role",
2742
+ permissions: "permission",
2743
+ sharingRules: "sharing_rule",
2744
+ policies: "policy",
2745
+ apis: "api",
2746
+ webhooks: "webhook",
2747
+ agents: "agent",
2748
+ skills: "skill",
2749
+ ragPipelines: "rag_pipeline",
2750
+ hooks: "hook",
2751
+ mappings: "mapping",
2752
+ analyticsCubes: "analytics_cube",
2753
+ connectors: "connector",
2754
+ data: "dataset"
2755
+ };
2756
+ var MetadataPlugin = class {
2757
+ constructor(options = {}) {
2758
+ this.name = "com.objectstack.metadata";
2759
+ this.type = "standard";
2760
+ this.version = "1.0.0";
2761
+ this.init = async (ctx) => {
2762
+ ctx.logger.info("Initializing Metadata Manager", {
2763
+ root: this.options.rootDir || process.cwd(),
2764
+ watch: this.options.watch,
2765
+ artifactSource: this.options.artifactSource?.mode
2766
+ });
2767
+ ctx.registerService("metadata", this.manager);
2768
+ console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
2769
+ const registerSysObjects = this.options.registerSystemObjects !== false;
2770
+ if (registerSysObjects) {
2771
+ try {
2772
+ const manifestService = ctx.getService("manifest");
2773
+ manifestService.register({
2774
+ id: "com.objectstack.metadata-objects",
2775
+ name: "Metadata Platform Objects",
2776
+ version: "1.0.0",
2777
+ type: "plugin",
2778
+ scope: "system",
2779
+ defaultDatasource: "cloud",
2780
+ objects: queryableMetadataObjects
2781
+ });
2782
+ ctx.logger.info("Registered system metadata objects", {
2783
+ queryable: queryableMetadataObjects.map((object) => object.name)
2784
+ });
2785
+ } catch {
2786
+ }
2787
+ }
2788
+ ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
2789
+ mode: this.options.artifactSource?.mode ?? "file-system",
2790
+ features: ["watch", "multi-format", "query", "overlay", "type-registry"]
2791
+ });
2792
+ };
2793
+ this.start = async (ctx) => {
2794
+ const src = this.options.artifactSource;
2795
+ const mode = this.options.config?.bootstrap ?? "eager";
2796
+ ctx.logger.info("[MetadataPlugin] Bootstrapping metadata", {
2797
+ bootstrap: mode,
2798
+ artifactSource: src?.mode ?? "none"
2799
+ });
2800
+ if (mode === "artifact-only") {
2801
+ if (src?.mode === "local-file") {
2802
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2803
+ } else if (src?.mode === "artifact-api") {
2804
+ await this._loadFromArtifactApi(ctx, src);
2805
+ } else {
2806
+ throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
2807
+ }
2808
+ } else if (mode === "lazy") {
2809
+ if (src?.mode === "local-file") {
2810
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2811
+ } else if (src?.mode === "artifact-api") {
2812
+ await this._loadFromArtifactApi(ctx, src);
2813
+ } else {
2814
+ ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
2815
+ }
2816
+ } else {
2817
+ if (src?.mode === "local-file") {
2818
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2819
+ } else if (src?.mode === "artifact-api") {
2820
+ await this._loadFromArtifactApi(ctx, src);
2821
+ } else {
2822
+ await this._loadFromFileSystem(ctx);
2823
+ }
2824
+ }
2825
+ try {
2826
+ const realtimeService = ctx.getService("realtime");
2827
+ if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
2828
+ ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
2829
+ this.manager.setRealtimeService(realtimeService);
2830
+ }
2831
+ } catch (e) {
2832
+ ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
2833
+ error: e.message
2834
+ });
2835
+ }
2836
+ };
2837
+ this.options = {
2838
+ watch: true,
2839
+ ...options
2840
+ };
2841
+ const rootDir = this.options.rootDir || process.cwd();
2842
+ const bootstrapMode = this.options.config?.bootstrap ?? "eager";
2843
+ const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? true;
2844
+ this.manager = new NodeMetadataManager({
2845
+ rootDir,
2846
+ watch: effectiveWatch,
2847
+ formats: ["yaml", "json", "typescript", "javascript"]
2848
+ });
2849
+ this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
2850
+ }
2851
+ /**
2852
+ * Fetch JSON content from a URL with configurable timeout.
2853
+ */
2854
+ async _fetchJson(url, fetchTimeoutMs, token) {
2855
+ const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
2856
+ const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
2857
+ const controller = new AbortController();
2858
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
2859
+ try {
2860
+ const headers = { Accept: "application/json, */*;q=0.5" };
2861
+ if (token) headers.Authorization = `Bearer ${token}`;
2862
+ const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
2863
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
2864
+ const content = await res.text();
2865
+ return JSON.parse(content);
2866
+ } catch (e) {
2867
+ if (e?.name === "AbortError") {
2868
+ throw new Error(
2869
+ `fetch timed out after ${timeoutMs}ms \u2014 set artifactSource.fetchTimeoutMs or OS_ARTIFACT_FETCH_TIMEOUT_MS to extend it (0 disables)`
2870
+ );
2871
+ }
2872
+ throw e;
2873
+ } finally {
2874
+ if (timer) clearTimeout(timer);
2875
+ }
2876
+ }
2877
+ /**
2878
+ * Parse raw artifact JSON (envelope or bare definition) and register all
2879
+ * metadata items into the MetadataManager.
2880
+ */
2881
+ async _parseAndRegisterArtifact(ctx, raw, label) {
2882
+ const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
2883
+ const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
2884
+ let metadata;
2885
+ const obj = raw;
2886
+ if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
2887
+ const artifact = ProjectArtifactSchema.parse(obj);
2888
+ metadata = artifact.metadata;
2889
+ } else if (obj?.success && obj?.data?.metadata) {
2890
+ const artifact = ProjectArtifactSchema.parse(obj.data);
2891
+ metadata = artifact.metadata;
2892
+ } else {
2893
+ const def = ObjectStackDefinitionSchema.parse(obj);
2894
+ const canonical = JSON.stringify(def, Object.keys(def).sort());
2895
+ const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
2896
+ const projectId = this.options.projectId ?? "proj_local";
2897
+ ProjectArtifactSchema.parse({
2898
+ schemaVersion: "0.1",
2899
+ projectId,
2900
+ commitId: "local-dev",
2901
+ checksum,
2902
+ metadata: def
2903
+ });
2904
+ metadata = def;
2905
+ }
2906
+ const memLoader = new MemoryLoader();
2907
+ const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
2908
+ let totalRegistered = 0;
2909
+ for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
2910
+ const items = metadata[field];
2911
+ if (!Array.isArray(items) || items.length === 0) continue;
2912
+ for (const item of items) {
2913
+ const name = item?.name;
2914
+ if (!name) continue;
2915
+ if (manifestPackageId && item._packageId === void 0) {
2916
+ item._packageId = manifestPackageId;
2917
+ }
2918
+ await memLoader.save(metaType, name, item);
2919
+ await this.manager.register(metaType, name, item);
2920
+ totalRegistered++;
2921
+ }
2922
+ }
2923
+ this.manager.registerLoader(memLoader);
2924
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
2925
+ return totalRegistered;
2926
+ }
2927
+ async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs) {
2928
+ const isUrl = /^https?:\/\//i.test(filePath);
2929
+ ctx.logger.info(
2930
+ `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2931
+ { path: filePath }
2932
+ );
2933
+ let raw;
2934
+ try {
2935
+ if (isUrl) {
2936
+ raw = await this._fetchJson(filePath, fetchTimeoutMs);
2937
+ } else {
2938
+ const content = await (0, import_promises.readFile)(filePath, "utf8");
2939
+ raw = JSON.parse(content);
2940
+ }
2941
+ } catch (e) {
2942
+ throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
2943
+ }
2944
+ await this._parseAndRegisterArtifact(ctx, raw, filePath);
2945
+ }
2946
+ /**
2947
+ * P2: Load metadata from the cloud artifact API endpoint.
2948
+ */
2949
+ async _loadFromArtifactApi(ctx, src) {
2950
+ const projectId = this.options.projectId;
2951
+ if (!projectId) {
2952
+ throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
2953
+ }
2954
+ let artifactUrl = src.url.replace(/\/+$/, "");
2955
+ if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
2956
+ artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
2957
+ }
2958
+ if (src.commitId) {
2959
+ artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
2960
+ }
2961
+ ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
2962
+ let raw;
2963
+ try {
2964
+ raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
2965
+ } catch (e) {
2966
+ throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
2967
+ }
2968
+ await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
2969
+ }
2970
+ async _loadFromFileSystem(ctx) {
2971
+ ctx.logger.info("Loading metadata from file system...");
2972
+ const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
2973
+ let totalLoaded = 0;
2974
+ for (const entry of sortedTypes) {
2975
+ try {
2976
+ const items = await this.manager.loadMany(entry.type, {
2977
+ recursive: true,
2978
+ patterns: entry.filePatterns
2979
+ });
2980
+ if (items.length > 0) {
2981
+ for (const item of items) {
2982
+ const meta = item;
2983
+ if (meta?.name) {
2984
+ await this.manager.register(entry.type, meta.name, item);
2985
+ }
2986
+ }
2987
+ ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
2988
+ totalLoaded += items.length;
2989
+ }
2990
+ } catch (e) {
2991
+ ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
2992
+ }
2993
+ }
2994
+ ctx.logger.info("Metadata loading complete", {
2995
+ totalItems: totalLoaded,
2996
+ registeredTypes: sortedTypes.length
2997
+ });
2998
+ }
2999
+ };
3000
+
2748
3001
  // src/loaders/remote-loader.ts
2749
3002
  var RemoteLoader = class {
2750
3003
  constructor(baseUrl, authToken) {
@@ -2842,6 +3095,9 @@ var RemoteLoader = class {
2842
3095
  }
2843
3096
  };
2844
3097
 
3098
+ // src/index.ts
3099
+ var import_metadata3 = require("@objectstack/platform-objects/metadata");
3100
+
2845
3101
  // src/routes/history-routes.ts
2846
3102
  function registerMetadataHistoryRoutes(app, metadataService) {
2847
3103
  app.get("/api/v1/metadata/:type/:name/history", async (c) => {
@@ -2997,7 +3253,8 @@ var HistoryCleanupManager = class {
2997
3253
  async runCleanup() {
2998
3254
  const driver = this.dbLoader.driver;
2999
3255
  const historyTableName = this.dbLoader.historyTableName;
3000
- const tenantId = this.dbLoader.tenantId;
3256
+ const organizationId = this.dbLoader.organizationId;
3257
+ const projectId = this.dbLoader.projectId;
3001
3258
  let deleted = 0;
3002
3259
  let errors = 0;
3003
3260
  try {
@@ -3008,8 +3265,11 @@ var HistoryCleanupManager = class {
3008
3265
  const filter = {
3009
3266
  recorded_at: { $lt: cutoffISO }
3010
3267
  };
3011
- if (tenantId) {
3012
- filter.tenant_id = tenantId;
3268
+ if (organizationId) {
3269
+ filter.organization_id = organizationId;
3270
+ }
3271
+ if (projectId !== void 0) {
3272
+ filter.project_id = projectId;
3013
3273
  }
3014
3274
  try {
3015
3275
  const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
@@ -3021,9 +3281,12 @@ var HistoryCleanupManager = class {
3021
3281
  }
3022
3282
  if (this.policy.maxVersions) {
3023
3283
  try {
3284
+ const baseWhere = {};
3285
+ if (organizationId) baseWhere.organization_id = organizationId;
3286
+ if (projectId !== void 0) baseWhere.project_id = projectId;
3024
3287
  const metadataIds = await driver.find(historyTableName, {
3025
3288
  object: historyTableName,
3026
- where: tenantId ? { tenant_id: tenantId } : {},
3289
+ where: baseWhere,
3027
3290
  fields: ["metadata_id"]
3028
3291
  });
3029
3292
  const uniqueIds = /* @__PURE__ */ new Set();
@@ -3033,10 +3296,7 @@ var HistoryCleanupManager = class {
3033
3296
  }
3034
3297
  }
3035
3298
  for (const metadataId of uniqueIds) {
3036
- const filter = { metadata_id: metadataId };
3037
- if (tenantId) {
3038
- filter.tenant_id = tenantId;
3039
- }
3299
+ const filter = { metadata_id: metadataId, ...baseWhere };
3040
3300
  try {
3041
3301
  const historyRecords = await driver.find(historyTableName, {
3042
3302
  object: historyTableName,
@@ -3110,20 +3370,22 @@ var HistoryCleanupManager = class {
3110
3370
  async getCleanupStats() {
3111
3371
  const driver = this.dbLoader.driver;
3112
3372
  const historyTableName = this.dbLoader.historyTableName;
3113
- const tenantId = this.dbLoader.tenantId;
3373
+ const organizationId = this.dbLoader.organizationId;
3374
+ const projectId = this.dbLoader.projectId;
3114
3375
  let recordsByAge = 0;
3115
3376
  let recordsByCount = 0;
3116
3377
  try {
3378
+ const baseWhere = {};
3379
+ if (organizationId) baseWhere.organization_id = organizationId;
3380
+ if (projectId !== void 0) baseWhere.project_id = projectId;
3117
3381
  if (this.policy.maxAgeDays) {
3118
3382
  const cutoffDate = /* @__PURE__ */ new Date();
3119
3383
  cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
3120
3384
  const cutoffISO = cutoffDate.toISOString();
3121
3385
  const filter = {
3122
- recorded_at: { $lt: cutoffISO }
3386
+ recorded_at: { $lt: cutoffISO },
3387
+ ...baseWhere
3123
3388
  };
3124
- if (tenantId) {
3125
- filter.tenant_id = tenantId;
3126
- }
3127
3389
  recordsByAge = await driver.count(historyTableName, {
3128
3390
  object: historyTableName,
3129
3391
  where: filter
@@ -3132,7 +3394,7 @@ var HistoryCleanupManager = class {
3132
3394
  if (this.policy.maxVersions) {
3133
3395
  const metadataIds = await driver.find(historyTableName, {
3134
3396
  object: historyTableName,
3135
- where: tenantId ? { tenant_id: tenantId } : {},
3397
+ where: baseWhere,
3136
3398
  fields: ["metadata_id"]
3137
3399
  });
3138
3400
  const uniqueIds = /* @__PURE__ */ new Set();
@@ -3142,10 +3404,7 @@ var HistoryCleanupManager = class {
3142
3404
  }
3143
3405
  }
3144
3406
  for (const metadataId of uniqueIds) {
3145
- const filter = { metadata_id: metadataId };
3146
- if (tenantId) {
3147
- filter.tenant_id = tenantId;
3148
- }
3407
+ const filter = { metadata_id: metadataId, ...baseWhere };
3149
3408
  const count = await driver.count(historyTableName, {
3150
3409
  object: historyTableName,
3151
3410
  where: filter